Why text that looks the same is not the same
A string is a sequence of code points, and a great deal of what can go wrong with text is invisible on screen. Two strings can render identically and still fail an equality check, a database lookup, a filename match or a password comparison. This tool exists to make the difference visible: it lists every code point, shows the bytes each one becomes, and highlights the specific characters that cause trouble.
Zero-width and format characters
U+200B ZERO WIDTH SPACE, U+200C/D joiners, U+2060 WORD JOINER, U+FEFF (the byte-order mark, which also appears mid-string when files are concatenated) and the bidirectional controls U+202A–U+202E and U+2066–U+2069 all render as nothing. They arrive via copy-paste from PDFs, web pages and chat apps, and they break string comparison, regexes, CSV parsing and JSON keys. The findings panel counts them, the table marks them, and the clean-copy button strips them.
Lookalikes across scripts
Cyrillic а (U+0430), Greek ο (U+03BF) and Latin a and o are visually identical in most fonts. A username, domain or package name mixing scripts inside one word is the classic homograph attack — раypal with two Cyrillic letters. The tool detects a word containing letters from more than one script and names the scripts, which is the same check IDNA-aware browsers apply to domain names.
Normalization
é can be one code point (U+00E9) or two (U+0065 U+0301, e plus combining acute). Both display the same; they are different strings. NFC composes, NFD decomposes, and the K forms additionally fold compatibility characters (a ligature fi to fi, a full-width A to A). The table shows the length of your text in each form and whether it is already normalized, and which form to store is discussed below.
Bytes, units and graphemes
"Length" has four answers. A single flag emoji 🇦🇪 is one grapheme (what a user sees), two code points (two regional indicators), four UTF-16 units (what JavaScript's .length counts) and eight UTF-8 bytes (what a database column stores). A VARCHAR(10) that is really 10 bytes holds one such flag and nothing else. The statistics strip shows all four counts at once.
Which normalization form to store
Normalize on input, once, and store NFC. It is the form most text arrives in, the form the W3C recommends for the web, and the form that keeps é as a single code point so length checks and index prefixes behave. Use NFKC only for identifiers that must be compared loosely — usernames, search keys — because it is lossy: it folds superscripts to digits and removes distinctions that are sometimes meaningful (² becomes 2). Never compare strings for equality without agreeing on a form; PostgreSQL, MySQL and most languages provide a normalize() function, and JavaScript has String.prototype.normalize(), which is what this page uses.
macOS is the notable exception on the file system: HFS+ stored filenames in a variant of NFD, so a file created on a Mac and listed from Linux could have a decomposed name that does not match the composed name in your database. APFS preserves what it is given but compares normalization-insensitively. If a filename check works on one machine and fails on another, paste the name here and compare the forms.
Characters worth refusing at the door
| Code point | Name | Why it matters |
U+200B | Zero width space | Invisible; splits words for search and breaks exact matching. Very common in copied text. |
U+FEFF | Byte order mark | Legitimate as the first character of a file; anywhere else it is garbage that breaks JSON, YAML and shebang lines. |
U+00A0 | No-break space | Looks like a space, is not one to split(' ') or trim() in some languages. Word processors insert it freely. |
U+202E | Right-to-left override | Reverses display order of what follows: invoicefdp.exe shows as invoiceexe.pdf. Used in file-name and source-code attacks (the "Trojan Source" class). |
U+2028 / U+2029 | Line / paragraph separator | Valid in JSON, historically illegal inside a JavaScript string literal, so embedding JSON in a script tag could throw. |
U+0000 | Null | Terminates strings in C; many databases and file systems reject it. Its presence usually means binary data was read as text. |
U+FFFD | Replacement character | Not a problem in itself: it is the evidence of one. It appears where a decoder met bytes that were not valid in the encoding it assumed, usually Latin-1 read as UTF-8 or the reverse. |
How the inspection works
The string is walked by code point (for…of, not by UTF-16 index), each code point is classified with Unicode property escapes in regular expressions — \p{L}, \p{Script=Cyrillic}, \p{Cf} and so on — and graphemes come from Intl.Segmenter, the browser's implementation of the Unicode grapheme cluster rules. UTF-8 bytes are produced by TextEncoder. Normalization uses String.prototype.normalize(). Everything runs locally, and the character names shown for the flagged code points come from a small table in the page rather than the full Unicode name database, which is why only the notable ones are named.