Zalgo works by stacking combining marks on top of each letter. This removes those marks and leaves the base characters standing.
A combining mark is not a character that sits beside the letter before it. It attaches to that letter and, critically, it has zero advance width: the renderer draws it and then does not move the pen forward. Stack thirty of them on one base letter and the text engine still treats the run as a single letter wide, so all thirty are drawn on top of each other, climbing away from the baseline. Nothing in the layout accounts for that vertical growth. The line box was sized from the font's ascent and descent, and browsers compute line height from those metrics rather than from the ink actually drawn, so the marks simply overflow it. That is the whole visual effect. Zalgo text does not push its neighbours apart; it paints over them. In a feed with fixed row heights the spikes cross into the post above and the post below. In a container with overflow hidden, the same string gets sliced off mid-mark instead. Rendering also varies by engine, so the same string is not equally dramatic everywhere. Once you know it is overflow rather than a font, the fix stops being a mystery: remove the marks and every base character underneath is untouched, still in its original order, because nothing about the letters themselves was ever changed.
Most generators pull from a fixed pool. The core of it is Combining Diacritical Marks, U+0300 to U+036F, which holds the ordinary accents: U+0300 grave, U+0301 acute, U+0303 tilde, U+0323 dot below. Those produce the classic up-and-down spikes, because the block is split between marks that render above the base and marks that render below it. Beyond that, Combining Diacritical Marks Extended occupies U+1AB0 to U+1AFF, Combining Diacritical Marks Supplement occupies U+1DC0 to U+1DFF, and Combining Diacritical Marks for Symbols, U+20D0 to U+20FF, adds arrows, rings and enclosing shapes that were encoded to annotate mathematical notation rather than letters. Combining Half Marks at U+FE20 to U+FE2F round out the usual set. Scripts other than Latin contribute too, since Cyrillic, Hebrew and Arabic all have their own combining ranges, and a generator that wants more variety reaches into them. This is why counting blocks is a losing game if your plan is to enumerate characters. There is no single zalgo block to filter on. The marks are scattered across the standard by script and by era of encoding, and new ones are added in later Unicode versions, so a list written today is already incomplete for a string produced tomorrow.
The correct algorithm has two steps, and neither of them is a list of characters. First, normalise. Unicode defines four normalisation forms in UAX #15, and the decomposing ones take every precomposed character apart into a base plus its marks. The single character U+00E9, e with acute, becomes U+0065 followed by U+0301. That matters because zalgo strings are usually a mix: some accents arrived precomposed and some as separate marks, and until you decompose you cannot see them all as the same thing. Second, filter by general category. Every code point in the standard carries a General_Category value, and the nonspacing marks are exactly those assigned Mark, Nonspacing (Mn). Dropping every code point in that category removes the stack in one pass, whatever block it came from and whichever Unicode version introduced it. In JavaScript this is a normalize call followed by a regular expression using Unicode property escapes, which have been in the language since ES2018. The property data is maintained by the Unicode Consortium and updated with each release, so the filter inherits new characters for free. A blocklist cannot do that. It only ever knows the characters its author happened to think of on the day it was written.
People reach for NFC first because it is the form most systems store text in, and it sounds like the tidy one. It is not a cleaner. NFC applies canonical decomposition and then canonical composition: it puts base and mark back together wherever a single precomposed character exists for that pair. So e followed by U+0301 becomes U+00E9, one character instead of two, and the string gets shorter. The marks are still there. They are just packaged differently. A zalgo string carries thirty marks on one base and the standard has a precomposed character for at most one of those pairs, so NFC composes that one and leaves the rest exactly where they were. The visual result is unchanged. The choice that does matter is NFD versus NFKD. NFD only undoes canonical equivalences, which is the conservative option. NFKD also applies compatibility mappings, and those go further: characters in the Mathematical Alphanumeric Symbols block, U+1D400 to U+1D7FF, map back to plain ASCII letters, superscripts fold to ordinary digits, and ligatures split apart. If the pasted text is zalgo layered over styled unicode letters, NFKD flattens both at once. If you wanted the styling kept, that is destruction rather than cleaning. Pick NFD when the base text is ordinary prose.
Here is the part most cleaners do not tell you: removing every Mn character is destructive to real writing. Vietnamese is the clearest case. It routinely carries two marks on one vowel, a tone mark over a vowel that already has a circumflex, so U+1EBF decomposes to U+0065, U+0302, U+0301. Two nonspacing marks, on one letter, in a perfectly ordinary word. A filter that treats stacked marks as suspicious sees Vietnamese as an attack. Thai tone marks and the vowel signs written above the consonant are nonspacing too, and removing them does not merely drop an accent; it changes the word. Arabic vocalisation, the harakat used in Qur'anic text and in books for learners, is nonspacing. Hebrew niqqud is nonspacing. Devanagari's virama, the mark that suppresses an inherent vowel and forms conjunct consonants, is nonspacing as well, so a blanket strip silently rewrites Hindi, Marathi and Sanskrit. None of this makes the tool wrong. It makes the tool narrow. Use it on text you know was Latin prose before somebody vandalised it, and read the output before you publish. If the input might be in a language whose writing system depends on combining characters, do not run a blanket strip over it at all.
The reasons to clean zalgo are practical and mostly not about taste. The first is layout. Pasted zalgo in a card, a table cell or a chat bubble overflows into whatever is drawn near it, and if that surface is yours, one user's paste degrades the page for everyone reading it. The second is search. Substring matching is exact, so a search for hello will not match h-e-l-l-o with marks interleaved between the letters, and the text becomes invisible to your own site search. Cleaning makes it findable again. The third is assistive technology. Screen readers handle stacked marks inconsistently: some announce each one by name, which turns a five-letter word into a minute of speech, and some skip them and read something the sighted reader never saw. The fourth is form validation. Fields that count characters are usually counting UTF-16 code units, and a short zalgo word can be several hundred of them, so a bio that looks like one word fails a limit meant for a sentence, while validators restricted to certain scripts reject it outright. The fifth is moderation, where a human reviewer needs to read what was actually posted rather than guess at the letters buried under the noise.
Zalgo does not execute anything. It does not escape a text field or inject markup. Every character in it is a valid, standards-conformant code point doing exactly what the standard says it does, and the mess is entirely a consequence of how renderers place ink relative to a baseline. Stored in a database, a zalgo string is just a longer string. There is a real edge, though, and it is a resource one. Text shaping is work per mark: the engine positions each one, checks it against the marks already placed, and draws it. Push that count into the thousands on a single base character and rendering time climbs steeply. Strings built that way have been used to make chat clients, browsers and mobile keyboards stall or crash while trying to display a message, which is a denial of service by rendering cost rather than by anything clever. Unicode's own security guidance treats long runs of combining marks as something identifier systems should restrict, which is one reason usernames on many platforms reject them. Treat zalgo as untrusted display input: cap the number of marks per base character on the way in, and you avoid both the layout damage and the pathological case.
Stripping is one-way. The tool holds no record of what it removed, so if the input contained a legitimate accent it is gone and nothing in the output tells you it was ever there. Keep the original somewhere before you clean anything you cannot retype. There are also characters an Mn filter deliberately does not touch. Enclosing marks (Me), such as U+20DD combining enclosing circle, and spacing combining marks (Mc), which include several Devanagari vowel signs, are separate categories, so a strict Mn strip leaves them in place. So do the format characters in category Cf, including zero-width space U+200B, zero-width joiner U+200D and the bidirectional overrides, which are invisible rather than decorative and are a different problem with a different fix. A string can come out of a mark cleaner looking perfect and still carry them. Finally, cleaning does not restore intent. If a word was already misspelled, or the base letters were themselves substitutes drawn from another block, the output is a faithful copy of that rather than a corrected version. Everything here runs in your browser on the text you paste, nothing is uploaded, and there is no account, so the only copy of the original is the one you kept.
Not the emoji themselves, but be careful: variation selectors in the range U+FE00 to U+FE0F are nonspacing marks, and U+FE0F is what tells a system to show the colour emoji rather than the monochrome text glyph. A strict Mn strip removes it, so a few symbols can switch to their plain black and white form. Skin tone modifiers are a different category and survive.
No. The cleaner produces new text and keeps no history of the marks it dropped, and your browser's undo only covers edits you made in the box yourself. Re-running a zalgo generator on the cleaned output gives you a fresh random stack, not the sequence you had before. Copy the original into a scratch file first if it matters to you.
Because a counter and your eye measure different things. What looks like one letter is a grapheme cluster, defined in UAX #29, and it can be any number of code points long. Most fields count code units instead, so a base letter plus twelve marks costs thirteen against the limit while occupying the space of a single character on screen.
Validate on input rather than cleaning later. A regular expression that rejects any run of three or more consecutive characters in the mark categories catches zalgo while leaving Vietnamese and Thai alone, since those orthographies rarely stack more than two. Tune the threshold to the languages you accept, and apply it on the server as well, because client-side checks are trivially skipped.
No. Only characters in the mark categories are removed. Spaces, tabs, newlines and punctuation pass through unchanged, and word order is never altered. That matters if you are cleaning a pasted block of code or a table, where a tool that also trimmed whitespace would break the structure you were trying to preserve. Paragraph breaks survive the round trip intact.
Crawlers index the code points that are genuinely in the page, so a heading stuffed with combining marks is not the same string as the word you meant to write. Anything that relies on exact matching, from your own site search to a browser's find-on-page, misses it. Clean before you publish, because there is no rendering trick that undoes it afterwards.
The cleaner works on everything in the box, so paste only the affected portion if the rest contains accents you need to keep. That is the safest workflow for mixed-language documents: clean the vandalised paragraph on its own, then put it back where it belongs. Comparing before and after side by side is worth the extra minute on anything long.
Then a mark stripper will not help you. Look instead for invisible format characters such as U+200B and U+200D, right-to-left overrides that scramble the apparent word order, or letters substituted from another block that your font cannot draw and renders as boxes. Those need a plain-text conversion or an invisible-character remover, which is a different operation entirely.