A string can look empty and still not be empty. It can look like a single space and still fail a validation check, break a search, or disappear in one app and show up as a box in another. Most of the time, the culprit is not a bug in your rendering code. It is an invisible Unicode character doing exactly what it was designed to do, in a place where you did not expect it.
This is a practical tour of those characters: what they are, why they behave differently from a normal space, and how to handle them deliberately instead of discovering them in production.
Not all blanks are spaces
A normal space is U+0020. It is visible as a gap, it separates words, and most text tools treat it as whitespace. Invisible characters are a different family, and they are not interchangeable.
Some of the most common ones are:
- U+200B, Zero Width Space: no visible width, but it can act as a line-break opportunity. It is often used to allow a long word or URL to wrap without inserting a visible space.
- U+200C, Zero Width Non-Joiner, and U+200D, Zero Width Joiner: they control joining behaviour. The joiner is also a core part of many emoji sequences, where it joins separate emoji code points into one displayed glyph.
- U+3164, Hangul Filler: a full-width blank that is visually empty but still occupies a character position. It is widely used in game nicknames and social bios where a normal space is rejected.
- U+2800, Braille Pattern Blank: another visually blank character that behaves differently across fonts and platforms, which is why it is sometimes used as an alternative when U+3164 is filtered out.
- U+00A0, No-Break Space: it looks like a space, but it prevents a line break at that point. It is also not trimmed by some tools in the same way as U+0020.
- U+FEFF, Zero Width No-Break Space / Byte Order Mark: it can appear at the start of a file or a pasted string and remain invisible in most editors, while still counting as a character.
The key point is that "invisible" does not mean "absent." Each of these characters has a code point, a general category in Unicode, and a set of rules for rendering, line breaking, and text segmentation. Your code, your database, and your user's device may each apply those rules differently.
Length is not what you see
One of the first surprises for developers is string length. In JavaScript, for example, a string containing a single Hangul Filler still has a length of 1, even though it renders as blank. A string with an emoji sequence joined by U+200D can have a much larger length than the number of visible glyphs.
const blank = "\u3164";
console.log(blank.length); // 1
console.log([...blank].length); // 1, but the visual width is still blank
const family = "👨👩👧";
console.log(family.length); // more than 1, because of joiners and code units
console.log([...family].length); // counts code points, not visible glyphs
This matters for validation. A form that only checks value.length > 0 will accept a field that contains only invisible characters. A form that only checks value.trim() === "" may also miss characters that a given language's trim function does not recognise as whitespace. If a username, bio, or display name is supposed to be non-empty, you need to decide explicitly what counts as empty for your product, and test that decision with invisible inputs, not just with an empty string and a normal space.
A useful pattern is to normalise first, then check the remaining code points against an explicit allowlist or blocklist for your use case, rather than relying on a generic whitespace check to do all the work.
Copy, paste, and the clipboard do not clean your text
Users rarely type invisible characters directly. They copy them from a tool, a chat app, a game, or another document, and paste them into your input. That is where many invisible-character bugs enter a system.
For example, a nickname that looks like it has a space in it may actually contain a Hangul Filler, because many games reject a normal space in a username. For example, SpasiFF provides a copy-and-test workflow for blank characters used in Free Fire nicknames, so a player can check whether a character is accepted before trying it in the game. The character is still a real Unicode code point when it is pasted; it is just rendered as blank.
The same principle applies to styled text. A related example is Tulisan Aesthetic, which generates styled Unicode variants for names and bios. Those variants are not a different font in the usual sense. They are different Unicode characters that look like styled letters, and they will be stored, searched, and read by screen readers as those characters, not as the plain letters they resemble.
If your system stores user text verbatim, it will store those code points verbatim too. That can be fine, but it should be a conscious choice. If you later search, sort, compare, or deduplicate that text, you need to know whether you are comparing visual appearance, normalised text, or raw code points, because those three comparisons can give three different answers.
Normalisation changes identity
Unicode normalisation is often treated as a cleanup step, but it can change whether two strings are considered equal. NFC and NFKC, for example, do not treat all visually similar characters as the same, and they do not remove zero-width characters by default.
const a = "e\u0301"; // e + combining acute accent
const b = "é"; // precomposed é
console.log(a === b); // false
console.log(a.normalize("NFC") === b.normalize("NFC")); // true
const withZeroWidth = "hello\u200Bworld";
console.log(withZeroWidth.normalize("NFC")); // zero-width space is still there
That last line is the important one. Normalisation is not a general invisible-character remover. If you want to remove or flag zero-width characters, you need an explicit step for them, and you need to decide whether that step is safe for your users. Removing U+200D from an emoji sequence, for example, would break the emoji. Removing U+200B from a long URL might change where it can wrap. A blanket strip is easy to write and easy to regret.
A safer approach is to handle invisible characters by category and by context:
- For identifiers and usernames, reject or flag characters that are invisible and not needed for that field, and explain the rejection in plain language.
- For display text and bios, allow them, but make sure length limits, search, and moderation tools account for them.
- For file names, URLs, and security-sensitive inputs, be stricter, because invisible characters can hide differences between two strings that look identical.
Accessibility and search are part of the same problem
Screen readers and search engines do not see text the way a sighted reader does. A blank character that looks like a gap to a sighted user may be read as nothing, skipped, or announced in an unexpected way, depending on the character and the tool. Styled Unicode letters can be even more confusing, because a screen reader may spell out the underlying character names instead of reading the intended word.
This is not a reason to ban invisible or styled characters outright. It is a reason to test with them. If your product lets users set a display name, try setting it to a single blank character, to a name with a zero-width joiner, and to a styled Unicode name, and then check what happens in search, in a list view, in a notification, and with a screen reader. The bugs you find there are usually cheaper to fix than the support tickets they would otherwise become.
Search has a similar issue. If a user searches for a plain word, should a styled Unicode version of that word match? If a user searches for a name that contains a zero-width space, should the search ignore that character? There is no universal answer, but there should be a deliberate answer in your search code, not an accidental one caused by a normalisation step you forgot was there.
A checklist for handling invisible text deliberately
If you are building anything that accepts user text, this checklist covers most of the practical cases:
- Define empty. Decide which code points count as empty for each field, and write tests for them, including U+200B, U+3164, U+2800, and U+00A0.
- Test paste, not just typing. Copy invisible characters from real tools and paste them into your forms, because that is how users actually get them.
- Check length in code points and in graphemes. Use a grapheme-aware length check for display limits, and a code-point check for storage and validation, and know which one you are using where.
- Do not rely on trim alone. Check what your language's trim, whitespace, and word-boundary definitions actually cover.
- Normalise consciously. Use NFC or NFKC where it helps, but do not assume it removes invisible characters.
- Strip only with a reason. If you remove a character, remove it because that field does not need it, and keep a test that proves you did not break emoji sequences or legitimate joining behaviour.
-
Log the code points, not just the visual string. When debugging, log
codePointAtvalues or escaped forms, because a visual log of an invisible character looks like nothing at all.
Invisible characters are not a trick to be feared, and they are not a hack to be used without thinking. They are a normal part of Unicode, with real uses in typography, in games, in social profiles, and in multilingual text. The difference between a useful blank space and a production bug is usually just whether the developer knew it was there.
Build for the characters you cannot see, and the text you can see will behave much more predictably.
Top comments (0)