Invisible characters. They render as nothing, so the text LOOKS correct and compares unequal. A zero-width space in a password field or a BOM at the front of a CSV are the usual causes of “but it looks identical”.
Spaces that are not the ordinary U+0020. Almost always pasted from a word processor or a web page. They survive a trim() that only strips ASCII whitespace, and they break a split on " ".
Typographic punctuation substituted by a word processor — curly quotes, en and em dashes, a single-character ellipsis. Fine in prose, and a syntax error in code, a CSV or a JSON string.
There are four answers to “how long is this”, and they are all different.
"👨👩👧" is 8 to JavaScript’s .length, 5 code points, 18 bytes in UTF-8, and 1 character to the person who typed it.
A VARCHAR(20) measures bytes. A form validator usually counts UTF-16 units. The user counts what they can see. Pick one, call it “length”, and a field that looks long enough starts rejecting somebody’s name.
Then there are the characters that are there and do not render. A zero-width space in a password. A BOM at the front of a CSV that breaks the first column header. A non-breaking space pasted out of Word. The text looks right and compares unequal.
Direction controls get their own heading here. They do not merely fail to render — they reorder the text around them, so what you read and what runs can differ.
Because four different things measure "length". A VARCHAR column and an HTTP header count BYTES. JavaScript's .length counts UTF-16 units. A character limit on a form usually means code points. And the person typing counts what they can see. For "👨👩👧" those are 18, 8, 5 and 1. Picking one and calling it the length is how a field that looks long enough rejects somebody's name.
Usually one of three things, and this finds all of them. An invisible character — a zero-width space, a soft hyphen, a BOM at the front of the file. A space that is not U+0020, typically a non-breaking space pasted from a word processor. Or a different normalisation form: "é" can be one code point or two, and the two versions render identically and compare unequal in almost every language.
They reorder the text around them. That makes them different in kind from a character that merely fails to render: a line of source code containing U+202E can read one way and execute another, which is the Trojan Source class of attack. They get their own finding, ranked above everything except already-broken text, rather than sitting in a list beside a smart quote.
It depends what the text is for. Smart quotes are correct in prose and a syntax error in JSON or a CSV. A non-breaking space is deliberate in "10 kg" and a bug in a database key. Astral characters are perfectly valid and only a problem if something downstream slices by .length. The tool names what is there and why it matters; whether it belongs is your call.
Because the full Unicode character database is megabytes and this engine ships with no dependencies at all. Every character worth warning about is named exactly. Everything else is identified by its Unicode block, which is factual and short. Inventing a plausible-sounding name would be worse than an honest one.
NFC is the composed normalisation form, and it is what the web defaults to. If you are comparing strings, using them as keys, or storing them for a uniqueness check, normalise first — otherwise two identical-looking values will both be accepted and your duplicate check will never fire. The button appears only when the text is not already in NFC.
Runs entirely in your browser; nothing is transmitted. Full Unicode names are not shipped — every character worth warning about is named exactly, and the rest are identified by block rather than guessed at. Nothing you type here is sent to our servers — the calculation runs entirely in your browser.