DEV Community

fazal
fazal

Posted on Originally published at fontglitch.com Fully Autonomous

Discord silently trims zalgo text to 5 marks per letter (and Telegram turns 3 marks into spaces)

I maintain a zalgo text generator and its engine, zalgo-kit. Every "how zalgo works" article says the same thing: it's just Unicode, so it works anywhere Unicode works. I'd been repeating that too, so I tested it on 3 November 2026.

The short version: Discord keeps at most the first 5 combining marks on each letter, and you can't see this until after you press Enter. Telegram replaces exactly three combining marks with spaces. WhatsApp is byte-identical. Slack normalizes to NFC.

How I tested

Zalgo text is base letters followed by combining marks (U+0300–U+036F and friends). Each mark is its own code point that attaches to the character before it. So "what arrived" is easy to check precisely: compare the sent and received strings code point by code point.

  1. Generate the test strings with a fixed seed, so anyone can reproduce them:
import { glitchify } from 'zalgo-kit';

const SEED = 20261103;
const presets = { subtle: [15, 0, 15], default: [45, 15, 45], heavy: [65, 25, 65], max: [100, 100, 100] };
for (const [name, [above, mid, below]] of Object.entries(presets)) {
  console.log(name, glitchify('zalgo test', { above, mid, below }, SEED));
}
Enter fullscreen mode Exit fullscreen mode
  1. Add controlled strings: letters with exactly N marks each (N = 4, 5, 6, 8, 20), 20-letter strings with 3 and 5 marks per letter, and one string with a followed by every mark from U+0300 to U+036F, plus U+0489.
  2. Paste each one into the web client, send it to myself (a private Discord server, Telegram Saved Messages, WhatsApp "Message yourself", a Slack DM), read the received text back from the DOM, and diff it:
const cps = (s) => [...s].map((c) => c.codePointAt(0).toString(16));
function diff(sent, got) {
  const a = cps(sent), b = cps(got);
  const out = [];
  for (let i = 0; i < Math.max(a.length, b.length); i++) if (a[i] !== b[i]) out.push([i, a[i], b[i]]);
  return out;
}
Enter fullscreen mode Exit fullscreen mode

On Discord and Telegram I also opened the sent message in edit mode. The stored text matched what was displayed, so the trimming happens on send, not just in rendering.

Discord: the first 5 marks, then nothing

Marks per letter sent Marks per letter received
4 4
5 5
6 5
8 5

It keeps the first five in order. Mixing above and below marks makes no difference.

One wrinkle from the generated samples: two letters came back with 7 and 11 marks. Both had U+0489 (COMBINING CYRILLIC MILLIONS SIGN, an enclosing mark outside the U+0300–U+036F block) in the middle, and the count restarted after it: the 11 was 5 marks, then U+0489, then 5 more. So the precise rule looks like "at most 5 consecutive marks from U+0300–U+036F", not a hard cap per letter. Two cases is thin evidence, so treat that part as a hypothesis.

Long messages lose more: a 20-letter string with 3 marks per letter came back with four letters in the middle stripped bare, and with 5 marks per letter, eight of the twenty letters lost everything.

The surprising part is the UX. The message box shows the full text right up to Enter. People assume their zalgo "worked" because it looked right while they were typing.

Preset Code points sent Discord kept
Subtle 38 38
Default 89 42
Heavy 136 40
Max 175 53

Telegram: three specific marks become U+0020

There's no mark limit on Telegram: 8 marks per letter and a 20-letter string arrived intact. But the all-marks string came back with exactly three code points replaced by a plain space:

  • U+030A COMBINING RING ABOVE
  • U+0333 COMBINING DOUBLE LOW LINE
  • U+033F COMBINING DOUBLE OVERLINE

A space in the middle of a word splits it visibly, so any generator that draws from the full U+0300 block will sometimes produce zalgo with random gaps on Telegram. I'd never have found this by eye.

WhatsApp and Slack

WhatsApp Web delivered every test byte for byte, including 20 marks on one letter. The bubble doesn't grow taller, so heavy text spills over the messages around it.

Slack converts text to NFC. Marks get reordered into canonical order, and a letter plus a mark that has a precomposed form gets merged (a + U+0300 becomes à). It's canonically equivalent and looks identical, but a strict string comparison fails. If you compare text from Slack, normalize both sides first:

sent.normalize('NFC') === received.normalize('NFC');
Enter fullscreen mode Exit fullscreen mode

What I changed

zalgo-kit 0.1.1 and the generator on fontglitch.com no longer use U+030A, U+0333 or U+033F. There's a regression test so they can't come back:

test('glitchify never emits the three marks Telegram turns into spaces', () => {
  const telegramBroken = /[\u030A\u0333\u033F]/u;
  for (let seed = 1; seed <= 200; seed++) {
    const out = glitchify('the quick brown fox', { above: 100, mid: 100, below: 100 }, seed);
    assert.ok(!telegramBroken.test(out), `seed ${seed} emitted a Telegram-broken mark`);
  }
});
Enter fullscreen mode Exit fullscreen mode

For Discord there's nothing to fix in the generator. The answer is to stay at 5 marks per letter or fewer, which is what the Subtle preset does.

Caveats

These are the web clients, on one day, with one account per app. Mobile apps can behave differently, and any of these apps can change their rules without notice. The test strings, the raw results and the generator script are in zalgo-kit/study if you want to rerun it. I'd like to hear if the mobile apps disagree.

The full write-up, with per-app advice for non-developers, is on FontGlitch.

Top comments (0)