unicodex explained

Unicode, explained — codepoints, bytes & invisible characters, and why your string length is wrong

One glyph is not one codepoint. One codepoint is not one byte. And two strings that look identical can refuse to match. This page walks through exactly why — with every number derived live from the browser's own engine, the same ground truth the unicodex inspector reads.

1 The layer confusion

One glyph is not one codepoint is not one byte

The question "how long is this string?" has four different, all-correct answers, depending on which layer you count. Here is the same family emoji, counted four ways.

👨‍👩‍👧‍👦 one family emoji
grapheme clusters what you see · Intl.Segmenter 1
codepoints Python 3 len() · Rust chars() 7
UTF-16 units JS .length · Java/C# length 11
UTF-8 bytes on the wire · DB storage 25

The seven codepoints are four faces — 👨 👩 👧 👦 — glued by three invisible ZWJ joiners. Strip the joiners and the one glyph falls apart into four. The flag 🇮🇳 tells the same story: 1 glyph, 2 codepoints, 4 UTF-16 units.

2 The characters you can't see

Invisible characters, revealed

Some codepoints have no visible glyph at all. They copy-paste silently, break exact matching, and — in one nasty case — reorder text on screen without changing a single byte. unicodex's motif is the reveal box: a dashed mint frame that makes the invisible visible, its abbreviation set in monospace inside.

HiZWSPthere

ZERO WIDTH SPACE U+200B

Zero pixels wide. "Hi​there" looks like one word but is eight codepoints, and never equals "Hithere". It hides in copied IDs, URLs and code.

invoice_RLOgnp.exe

RIGHT-TO-LEFT OVERRIDE U+202E

Flips display order. The bytes read invoice_gnp.exe, but the screen shows invoice_exe.png — the Trojan Source class of trick, in filenames and source code alike.

SaveNBSP₹500

NO-BREAK SPACE U+00A0

Looks exactly like a space, isn't one. Word-splitting and exact string matching both fail on it, and a leading BOM (U+FEFF) can make a parser reject valid JSON.

Paste any of these into unicodex and every flagged character is revealed inline in its own box — with its Unicode name, a one-line risk note and a citation to the document that defines it.

3 The same text, four ways

Normalization — NFC · NFD · NFKC · NFKD

Unicode lets the "same" text be encoded more than one way. Normalization collapses those to a single canonical form so comparisons work. There are two axes: canonical (NFC/NFD — same appearance) versus compatibility (NFKC/NFKD — same meaning, may change appearance).

café canonical · NFC ↔ NFD
NFDc a f e ◌́5 codepoints
NFCc a f é4 codepoints

These two look pixel-identical. One is é (a single codepoint); the other is e + a combining acute accent. They fail === — but normalize both to NFC and they're equal.

le compatibility · NFKC / NFKD
inputfi l e3 codepoints
NFKCfile4 codepoints

The single ligature codepoint (U+FB01) survives NFC untouched — but compatibility normalization decomposes it to plain f + i. Great for search; lossy for round-tripping.

unicodex shows all four forms of your input at once and marks which ones changed — so you can see, for any string, whether a normalization mismatch is what's breaking your comparison.

4 Letters that lie

Look-alikes — when a letter isn't the letter

Different alphabets contain characters drawn the same. Swap a couple in and you get a word that reads perfectly but is, byte-for-byte, something else entirely — the basis of homograph phishing domains.

Two words. Your eyes say equal. The bytes say no.
Latin paypal 70 61 79 70 61 6C
Cyrillic spoof раypal 440 430 79 70 61 6C
raw === ✗ never equal after NFC ✗ still not equal

The first two letters are Cyrillic er (U+0440) and a (U+0430) — genuinely different letters, not a normalization difference. No amount of normalizing will ever make them match the Latin word, which is exactly why the flag matters. CONFUSABLE

5 The straight answer

So, why is my string length wrong?

It isn't wrong. Every layer is telling the truth about a different thing — and the tool you're using picked one for you. Here's who counts what, on that one family emoji.

JS "👨‍👩‍👧‍👦".length 11 UTF-16 code units
Python len("👨‍👩‍👧‍👦") 7 codepoints
Go len("👨‍👩‍👧‍👦") 25 UTF-8 bytes
a human "how many characters?" 1 grapheme cluster

Four answers, all correct, none of them a bug. The fix is never to argue with the number — it's to know which number you have. unicodex lays all four in front of you for any text you paste, then shows the codepoints and bytes that produced them.

Private by construction

The text you inspect never leaves your browser

You paste sensitive things into a character inspector — tokens, keys, private messages. Both this page and unicodex ship a policy that makes the browser itself refuse to send them anywhere.

default-src 'self'; connect-src 'none'
  • No network calls are possiblefetch, XHR and WebSockets are refused by the browser, by policy, not by promise.
  • No server, no account, no analytics — nothing to log in to, nothing watching.
  • No external fonts, scripts or images — everything is same-origin or a data: URI.
  • Local only — this explainer stores just your theme choice; unicodex keeps optional autosaved input in localStorage, with one-tap clear.

! What this honestly is not

Honest limits

  • Counts are the host engine's. Grapheme segmentation follows this browser's Intl.Segmenter and its Unicode version — older engines can split Indic conjuncts differently. unicodex reports the engine's answer, not a universal truth.
  • Not a security verdict. The confusable and invisible-character catalogs are curated subsets — the absence of a flag is never proof a string is safe, and unicodex never says "safe".
  • Rendering ≠ identity. A glyph shown here may draw as tofu or differently on your device fonts. This is about what text is, not how any screen draws it.
  • Approachable, not exhaustive. This page teaches the mechanism with a handful of examples; Unicode is far larger. Use the live app to inspect your own real strings.

Paste your own string. Watch it come apart.

Free, offline, MIT-licensed. Every codepoint, byte and invisible character — revealed in your own browser, sent nowhere.

Open unicodex →

unicodex source on GitHub