Hidden text: nine of the fourteen ways to hide it still hand it over
Short answer: hidden text is text that is in the file but not on
the screen, and hiding it from the eye is a different job from hiding it from everything else.
Measured in Chromium across fourteen ways to make text invisible, nine still put the text
on the clipboard when you press Ctrl+A and Ctrl+C. White text on a white background,
color: transparent, opacity: 0, font-size: 0,
clip-path, text pushed off screen, the screen-reader-only clip pattern and text
sitting under an opaque box all survive the copy. Only four methods keep the text out of both the
clipboard and find-in-page — visibility: hidden, display: none,
the hidden attribute and an HTML comment — and all four are still in the
source.
Nothing tested yet.
The three buttons do three different things, which is the whole point. The copy test builds a selection over the preview exactly the way Ctrl+A does and reports what it produces; the find test runs the same routine find-in-page uses; the source test checks the markup itself. Run all three on the same method and you get the answer that matters, which is not “is it hidden” but “hidden from whom”.
Every method, measured against five readers
Each row is one page, one word, and five independent checks: does anything get painted, does select-all-then-copy carry the word, does find-in-page locate it, is it in the HTML source, and does it appear in the accessibility tree that a screen reader walks. Nothing was inferred from the spec — the eyes column is a pixel comparison of the element against a blank region of the same size.
| Method | Eyes | Ctrl+A + copy | Ctrl+F | View-source | Screen reader |
|---|---|---|---|---|---|
| no hiding at all | yes | yes | yes | yes | yes |
| color: #fff on white | no | yes | yes | yes | yes |
| color: transparent | no | yes | yes | yes | yes |
| opacity: 0 | no | yes | yes | yes | yes |
| font-size: 0 | no | yes | yes | yes | yes |
| clip-path: inset(100%) | no | yes | yes | yes | yes |
| text-indent: -9999px | no | yes | yes | yes | yes |
| position: absolute, left: -9999px | no | yes | yes | yes | yes |
| screen-reader-only clip | no | yes | yes | yes | yes |
| under an opaque white box | no | yes | yes | yes | yes |
| height: 0 with overflow: hidden | no | no | yes | yes | yes |
| user-select: none | yes | no | yes | yes | yes |
| aria-hidden="true" | yes | yes | yes | yes | no |
| visibility: hidden | no | no | no | yes | no |
| display: none | no | no | no | yes | no |
| hidden attribute | no | no | no | yes | no |
| HTML comment | no | no | no | yes | no |
Read the first ten rows again and the practical consequence is uncomfortable. Every technique that works by making the text look like nothing — same colour as the background, zero opacity, zero size, clipped, moved off screen, covered up — leaves the text completely intact for anybody who selects the page and pastes it somewhere plain. That is one action, it takes about two seconds, and it defeats nine of the fourteen methods on the list.
The three at the bottom are the only ones that genuinely withhold the text from a reader, and they all withhold it from everybody at once: no pixels, no copy, no find, no screen reader. What they cannot do is remove it from the file. Every one of them is still sitting in the markup, one Ctrl+U away.
Three methods run backwards
The interesting rows are the ones where yes and no land in unexpected places.
user-select: none is the only method in the set that hides text from the clipboard
while leaving it plainly visible. The word is on the screen, Ctrl+F finds it, view-source shows
it, a screen reader reads it — and a select-all and copy produces nothing for it. If what
you want is for people to read a thing without taking it, this is the tool; it is also the one
that quietly breaks the ordinary human act of copying a reference or a code snippet off a page.
It is worth knowing that the property is doing something narrower than its name suggests: it
changes what can be selected, not what can be seen.
aria-hidden="true" is the mirror image. The text is visible, copyable, findable and
in the source, and it is absent from the accessibility tree. It hides text from exactly one
reader. That is the correct way to mark something decorative, and it is the wrong way to hide
anything from a person who can see the screen — a mistake that gets made surprisingly
often by people who assume the attribute means “invisible”.
The screen-reader-only clip pattern is the third one, and it is the case where “hidden” means the opposite of what you would guess. The text is painted nowhere on screen, and it is fully present to the clipboard, to find-in-page and to a screen reader. This is the accessible technique for labelling a control that has no visible label. It also means that anything you hide this way is, for a machine reading the page, not hidden at all.
The one row where the browser disagrees with itself
height: 0 with overflow: hidden deserves its own paragraph, because
two reasonable checks give opposite answers on it.
Measured on a page holding that one hidden word: document.body.innerText
contains the word. A selection built over the whole body
does not. Neither does the real clipboard after Ctrl+A and Ctrl+C. And
find-in-page does find it.
So: findable but not copyable; present in innerText and absent from
Selection.toString(). If you have ever written a check for stray text that reads
innerText, this is the case where it reports something the user cannot actually
take, and if you have ever written one that copies and compares, this is the case where it
misses something the user can locate with Ctrl+F. The two most common heuristics for
“is there text here” disagree on the same element, and neither is wrong about what it
measures.
What git does with text you cannot see
Version control is where hidden text stops being a curiosity. A change consisting of one zero-width space inside a word produces this:
-The invoice total is 480 dollars.
+The invoice total isU+200B 480 dollars.
Two lines that render identically, and a stat of one insertion and one deletion. Reviewers see a
line that changed and cannot see what changed about it. git diff --word-diff is the
fix, and it is worth knowing why it works: the character turns is and
is+U+200B into two different words as far as the diff is concerned, so the
invisible edit becomes a visible one:
The invoice total [-is-]{+isU+200B+} 480 dollars.
The whitespace tooling is where it gets worse, because it behaves as though invisible characters were the kind of noise it is designed to suppress. Measured on three files that differ only by one trailing character:
| Trailing character | git diff --check | --ignore-all-space | --ignore-space-change |
|---|---|---|---|
| normal space U+0020 | exit 2, “trailing whitespace” | change disappears | change disappears |
| no-break space U+00A0 | exit 0, silent | 1 insertion, 1 deletion | 1 insertion, 1 deletion |
| zero-width space U+200B | exit 0, silent | 1 insertion, 1 deletion | 1 insertion, 1 deletion |
That is the whole problem in three rows. Git catches the trailing space you can see and says nothing about the two you cannot. The flags that exist to stop whitespace changes cluttering a diff suppress a change that is a normal space and leave a change that is a no-break space or a zero-width space fully reported. So the invisible edit is the one edit guaranteed to survive every filter meant to hide trivial ones.
A line made entirely of forty zero-width spaces is the extreme version. Git reports it as one
insertion and zero deletions, prints it as an added line that renders blank, and
--ignore-blank-lines does not hide it. In the file it is not blank at all: the
document went from 17 bytes to 138, so a line that looks empty in the diff carries 121 bytes.
Finding them is easy once you stop typing the character and name it instead.
git grep -P '\x{200B}' matched the file in this test, and so did
git grep -n given the character directly, which is worth remembering because the
usual advice is that you cannot search for something you cannot type. You can — the shell
will happily store it from a paste, and matching on the code point is the reliable form.
A redaction box is a drawing instruction, not a deletion
The most consequential version of this mistake is in documents. A PDF is a list of drawing instructions: put this text here, fill that rectangle there. Painting over text appends an instruction to the end of the list; it does not remove the one that drew the text.
Measured on a small PDF built for the purpose, holding one line of text and then a filled white rectangle drawn over it: the file was 630 bytes, the rectangle instruction accounted for 28 of them, and the phrase was still present byte-for-byte. An extractor that does nothing but walk the content stream and read the text operands returned the phrase from the “redacted” file without any trouble.
Doing it properly means deleting the text operand rather than covering it. That took the file to 604 bytes, the extractor returned nothing, and the rectangle stayed exactly where it was. Same appearance, 26 bytes different, one of them recoverable.
The general rule falls out of that: if hiding the text added bytes to the file, the text is still in the file. Removing it is the only operation that makes it gone, and a smaller file is the sign that it worked.
Comments, binaries and the text nobody meant to ship
An HTML comment is the standard way to keep notes in a Markdown file that renders on a code host, and it works exactly as advertised: no pixels, no copy, no find-in-page. What it does not do is keep the note private. The comment is in the file, in the source view, in every clone, and in every diff that touches the line next to it. This is the most common way ordinary working notes end up published, and unlike the CSS methods it is not a mistake anybody notices, because nothing on screen ever looked wrong.
Compiled programs have the same property by construction. Strings that are never printed still sit in the binary, and the standard utility for listing printable sequences in a file will show them. Easter eggs that ship inside every build from a language runtime are the benign end of that; credentials and internal host names are the other end.
And then there is the case that is not hiding at all. A no-break space at the end of a line, a soft hyphen a word processor inserted, an invisible character that came along in a paste — these are hidden text in the literal sense, and they are invisible to everybody, including the person who put them there. To find those, the hidden character scanner reports which code point and where, and the invisible character list covers the ones people paste on purpose.
How to check a page in ten seconds
Select everything and paste it into a plain text editor. That single action recovers nine of the fourteen methods above, including every one that works by making text look like the background. It is the highest-yield check there is, and it needs no tools.
Whatever survives that, Ctrl+F will not help with — the four methods that defeat copy also
defeat find-in-page. For those, look at the source, which catches all four. For a file on disk
rather than a page, cat -A renders anything outside printable ASCII in readable
notation, and cmp gives you the byte offset of the first difference when two files
look the same and are not.
If the text is going to a language model rather than a person, the cost is not whether it is visible but whether it is counted: invisible characters are tokenised like any other. See what hidden characters cost in tokens.
What this page does not tell you
Whether a search engine indexes hidden text, and what it then does to your ranking. Those are two different questions and the discussion around them has never settled: one side points to experiments showing the text gets indexed, the other points out that indexed and ranked are not the same thing and that a test run on a single small site cannot show a penalty either way. It is not something that can be settled from one machine, so it is not claimed here.
The table is also specific to one engine. It was measured in Chromium, and the columns that come from the rendering and accessibility layers are the ones most likely to differ elsewhere. The view-source and clipboard columns follow from how the markup is parsed and are more stable.
Measured 2026-09-28 with Chromium via Playwright and the Chrome DevTools accessibility protocol, Git 2.x on Windows via Git Bash, and hand-built PDF content streams parsed with Python 3.13.12. Related: find hidden characters in a file, the full list of invisible characters, what a browser does with characters you cannot see, what one does to a form field, why there is no empty character, the zero-width space remover, and tag characters that hide a whole ASCII message.