Skip to content

Guide · 11 min read · 2026-09-17

How to Find Zero Width Spaces and Invisible Characters

A zero width space is three bytes of trouble that your eyes cannot see. In UTF-8, U+200B is the byte sequence e2 80 8b, and it sits happily inside a word, a product code, an email address or a URL slug without changing a pixel on screen. The string looks correct. The comparison fails, the search returns nothing, the link 404s, and you stare at two identical-looking lines.

This is a working reference for finding invisible characters in text you already have: in a browser, in VS Code, on the command line, in a spreadsheet and in a CMS. Every command below was run against a file holding U+200B, U+00A0, U+202F, U+00AD and U+FEFF, with a note on what each one misses.

Which invisible codepoints show up, and where do they come from?

Key takeaways

  • Eight codepoints cause almost every invisible character problem: U+200B, U+200C, U+200D, U+2060, U+FEFF, U+00A0, U+202F and U+00AD.
  • VS Code highlights most of them by default through editor.unicodeHighlight.invisibleCharacters, and one regex in the Find box lists the rest.
  • On Linux, grep -P with \x{200B} works. macOS grep has no -P flag at all, so use perl, python3 or a byte search.
  • Spreadsheet TRIM and CLEAN do not remove U+00A0 or U+200B. That is the most common false fix.
  • Most invisible characters in pasted text come from web pages, Word, PDFs and CSV exports, not from a language model.

Not every invisible character is a mistake. U+200D is load bearing inside emoji, and U+200C is required in Persian, Arabic and several Indic scripts. Which one you have decides whether to delete it.

CodepointNameWhat it doesWhere it comes from
U+200BZero width spaceMarks a point where a line may break. No width.Copied web text, CMS editors, code that adds break hints to long strings.
U+200CZero width non-joinerStops two adjacent letters forming a joined shape.Correctly typed Persian, Arabic, Indic text. Often legitimate.
U+200DZero width joinerBinds neighbouring characters into one glyph.Inside pasted emoji. Family and profession emoji are built from it.
U+2060Word joinerForbids a line break there, with no width.Typesetting systems protecting a unit from wrapping.
U+FEFFByte order markAt a file start it flags encoding. Anywhere else, a stray with no width.Excel CSV exports, editors saving "UTF-8 with BOM", concatenated files.
U+00A0No-break spaceLooks like a space, refuses to wrap, not matched by \s in every engine.HTML  , Word, PDF text layers, option plus space on a Mac.
U+202FNarrow no-break spaceA thinner non-breaking space, visibly narrower at large sizes.French punctuation spacing, document converters, some model output.
U+00ADSoft hyphenInvisible until the line breaks there, when a hyphen appears from nowhere.Word hyphenation, PDF to text conversion, German and Dutch typesetting.

How do you find them in a browser, and what does zerowidthspace.me actually do?

The browser is where most people start, because the text is usually in a clipboard rather than a file. Two kinds of page get recommended and they are not the same thing.

The first kind is a generator. zerowidthspace.me is a generator, not a detector. Loading it shows a pair of angle brackets with a zero width space between them, the line "there is a zero width space between those angle brackets", and a click to copy control that confirms with "copied!". That is the whole tool, and it does it well: when you need a U+200B to paste somewhere, it hands you one without a trip through a character map. What it will not do is take text you paste in and tell you what is hiding inside it. People who search for it by name while holding a broken string need the opposite tool.

The second kind is a cleaner: you paste text, it reports what it found and hands back a version with those codepoints removed or replaced. Pick one that names which codepoints it found. "Removed 3 zero width characters and 2 no-break spaces" is a diagnosis. A silent rewrite is not.

If the text is on a page rather than in a clipboard, count it in the devtools console:

  • document.body.innerText.match(/[-]/g)?.length

That returns a number or undefined. It reads rendered text only, so it misses anything in an attribute, a hidden element or a script tag, and it leaves U+00A0 out on purpose: a normal page is full of legitimate ones.

Making invisible characters visible in VS Code

VS Code has shipped Unicode highlighting since version 1.63, on by default. The settings are editor.unicodeHighlight.invisibleCharacters, which highlights uncommon invisible characters, editor.unicodeHighlight.ambiguousCharacters, which flags characters confusable with ASCII (Cyrillic а against Latin a), and editor.unicodeHighlight.nonBasicASCII. Two more earn their keep: editor.unicodeHighlight.includeComments, because a stray codepoint in a comment survives review, and editor.unicodeHighlight.allowedCharacters, which stops the editor shouting about characters you want.

Highlighting tells you something is there. To list every hit, open Find with the regex toggle on and search:

  • [- ]

Use "Find All" for one file, or the search sidebar with the same pattern to sweep a repository. Replace with nothing to delete, or with a plain space when you are targeting U+00A0 and U+202F, which should become an ordinary space rather than vanish.

One caution. Do not run that replace blind across a project: it strips U+200D out of emoji and damages Persian or Arabic strings that rely on U+200C. Search first, read the hits, then narrow the class.

Finding and stripping them from the command line

On Linux, GNU grep with Perl expressions is the shortest route:

  • grep -nP '[\x{200B}-\x{200D}\x{2060}\x{FEFF}\x{00A0}\x{202F}\x{00AD}]' file.txt

On macOS that fails outright. The bundled BSD grep answers grep: invalid option -- P, because it has no PCRE support. Two portable alternatives:

  • Match the raw UTF-8 bytes, which sidesteps locale and regex flavour. LC_ALL=C grep -n $'\xe2\x80\x8b' file.txt finds U+200B. Swap in \xef\xbb\xbf for U+FEFF, \xc2\xa0 for U+00A0, \xe2\x80\xaf for U+202F and \xc2\xad for U+00AD.
  • Use Perl, present on macOS and nearly every Linux box. perl -CSD -ne 'print "$.: $_" if /[\x{00A0}\x{00AD}\x{200B}-\x{200F}\x{2060}\x{FEFF}]/' file.txt does the same job. The -CSD flag makes Perl read the input as UTF-8 rather than raw bytes. Leave it off and the pattern will not match.

Grep gives you the line, not the character, and the matched line prints looking perfectly normal. To name the codepoints, use Python:

  • python3 -c "import sys,unicodedata as u; [print(i,j,'U+%04X'%ord(c),u.name(c)) for i,l in enumerate(open(sys.argv[1],encoding='utf-8'),1) for j,c in enumerate(l) if ord(c) in (0x200B,0x200C,0x200D,0x2060,0xFEFF,0xA0,0x202F,0xAD)]" file.txt

That prints line, column, codepoint and official Unicode name per hit. For bytes, hexdump -C file.txt gives the full picture and sed -n l file.txt is the quick version: it renders every non-ASCII byte as an octal escape, so U+200B appears as \342\200\213. Skip cat -v, which mangles multibyte UTF-8 rather than showing clean escapes.

For stripping, one pass handles both halves of the job, deleting the zero width characters and turning the odd spaces into ordinary ones:

  • perl -CSD -pe 's/[\x{200B}-\x{200D}\x{2060}\x{FEFF}\x{00AD}]//g; s/[\x{00A0}\x{202F}\x{2007}\x{2009}]/ /g' in.txt > out.txt

Write to a new file and diff before overwriting anything. What this misses: Cyrillic and Greek lookalike letters, which are visible characters that merely resemble Latin ones, and files whose real problem is a mis-declared encoding.

Clean a block of text right now

Paste it into the free cleaner at aitextwatermarkremoval.com. It finds and removes invisible Unicode, non-standard spaces, lookalike letters and styled letterforms, shows counts of what it found, and gives you text you can copy straight back. No signup, nothing to install.

Why will TRIM and CLEAN not fix your spreadsheet?

A VLOOKUP fails, the advice online says to wrap the key in TRIM and CLEAN, and nothing changes. The definitions explain why: TRIM removes ordinary ASCII spaces (character 32), and CLEAN was written for the first 32 non-printing ASCII control characters. U+00A0 is character 160. U+200B is 8203. Neither was ever in scope.

What works is SUBSTITUTE with UNICHAR, in Excel 2013 and later and in Google Sheets. Count first, before you change anything:

  • Count zero width spaces in A2: =LEN(A2)-LEN(SUBSTITUTE(A2,UNICHAR(8203),""))
  • Identify the character at a position: =UNICODE(MID(A2,5,1)), which returns 160 for a no-break space and 8203 for a zero width space
  • Clean the four usual suspects at once: =TRIM(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(A2,UNICHAR(8203),""),UNICHAR(65279),""),UNICHAR(160)," "),UNICHAR(8239)," "))

The decimal values: 8203, 8204 and 8205 for the zero width space, non-joiner and joiner, 8288 for U+2060, 65279 for U+FEFF, 160 for U+00A0, 8239 for U+202F, 173 for U+00AD. Note the ordering: TRIM goes outside, after the no-break spaces have become real spaces, so it has something to collapse.

One more trap. A U+FEFF at the start of a CSV attaches to the first column header, so the import maps every column except that one and the error blames a missing field rather than an invisible byte. hexdump -C on the first line settles it in seconds.

Cleaning a CMS field before you publish

A CMS is the worst place to catch these: the editor renders your text faithfully, which means rendering invisible characters invisibly. Switching a WordPress block to "Edit as HTML" exposes   entities, but a literal U+200B stays as invisible in the code view as in the visual one.

Clean the text before it goes into the editor, not after. Once it sits in a post body, a database row and a rendered cache, you are chasing it through three layers. The fields that hurt most are the ones compared rather than read: URL slugs, redirect rules, tag names, email addresses in form settings, API keys pasted out of a dashboard. A slug with a zero width space produces a URL that looks right in the address bar and 404s forever.

To check a published page, fetch the raw HTML instead of trusting the browser view. curl -s https://example.com/page | grep -c $'\xe2\x80\x8b' counts source lines holding a zero width space: a straight yes or no on whether the problem reached production.

What most people get wrong

"Paste it into Notepad to strip it out." This turns up in every forum thread and it is wrong. A plain text editor strips formatting: fonts, colours, bold, links. It does not strip codepoints, because a zero width space is text and Notepad has no reason to discard text. Same for paste as plain text with Ctrl+Shift+V. Every invisible character makes the trip intact.

"Retyping the line will fix it." It will, if you retype the whole line and copy none of it. Most people retype the visible part and leave the surrounding text alone, which is where the character was.

"ChatGPT put it there." Usually not. Most arrive from HTML, Word documents, PDF text extraction, CSV exports and messaging apps. One real episode sits behind the belief. In April 2025, users reported U+202F narrow no-break spaces appearing in output from OpenAI's o3 and o4-mini models. OpenAI called it a quirk of large scale reinforcement learning, and it stopped within days. OpenAI has not shipped a text watermark.

"Removing these characters is the same as removing a watermark." It is not. Anthropic watermarks Claude output with SynthID-Text, announced on 11 August 2026, and Google has watermarked Gemini with SynthID since 2024. Both are statistical: the signal sits in patterns of word choice across a passage, not in any character you could search for. AI detectors such as GPTZero, Turnitin, Originality.ai and Copyleaks likewise score statistical properties of wording, which makes them a separate subject from characters. Character cleaning fixes broken comparisons, failed lookups, dead URLs, ragged line breaks and rejected form submissions. That is the job it does.

Frequently asked questions

How do I tell which invisible character I have, not just that I have one?

Get the codepoint, not the symptom. In a spreadsheet, =UNICODE(MID(A2,n,1)) returns a decimal value for the character at position n, and the Python snippet above prints position, codepoint and official name for every hit. Knowing you have U+00A0 rather than U+200B changes the fix: the first should become a normal space, the second should go.

Does zerowidthspace.me remove zero width spaces?

No. The live page is a generator: it holds one zero width space between two angle brackets and copies it to your clipboard on click, confirming with "copied!". Useful when you need to insert a U+200B. For the reverse job, use a cleaner or one of the editor and command line methods above.

Is it safe to delete every zero width character I find?

Not blindly. U+200D holds multi-part emoji together, so removing it splits one emoji into several, and U+200C is required for correct rendering in Persian, Arabic and several Indic scripts. Plain English with no emoji: deleting the class is safe. Anything else: read each hit first.

Why does my whitespace regex miss no-break spaces?

Because \s means different things in different engines. JavaScript does match U+00A0. Python 3 matches Unicode whitespace on str patterns but not on bytes. PCRE depends on UTF and UCP modes, and UCP is off by default. Naming the codepoints explicitly, as in [\x{00A0}\x{202F}\x{2007}\x{2009}], avoids the whole question.

Frequently asked questions

How do I tell which invisible character I have, not just that I have one?
Get the codepoint, not the symptom. In a spreadsheet, =UNICODE(MID(A2,n,1)) returns a decimal value for the character at position n, and the Python snippet above prints position, codepoint and official name for every hit. Knowing you have U+00A0 rather than U+200B changes the fix: the first should become a normal space, the second should go.
Does zerowidthspace.me remove zero width spaces?
No. The live page is a generator: it holds one zero width space between two angle brackets and copies it to your clipboard on click, confirming with "copied!". Useful when you need to insert a U+200B. For the reverse job, use a cleaner or one of the editor and command line methods above.
Is it safe to delete every zero width character I find?
Not blindly. U+200D holds multi-part emoji together, so removing it splits one emoji into several, and U+200C is required for correct rendering in Persian, Arabic and several Indic scripts. Plain English with no emoji: deleting the class is safe. Anything else: read each hit first.
Why does my whitespace regex miss no-break spaces?
Because \s means different things in different engines. JavaScript does match U+00A0. Python 3 matches Unicode whitespace on str patterns but not on bytes. PCRE depends on UTF and UCP modes, and UCP is off by default. Naming the codepoints explicitly, as in [\x{00A0}\x{202F}\x{2007}\x{2009}], avoids the whole question.