I run EmojiDig, an emoji search, so I spend a lot of time on how emoji are encoded. A thumbs-up with a skin tone is two code points. A family is seven. The Scotland flag is a black flag plus six invisible tag characters. I wanted to know whether chat apps deliver those sequences intact, so on 4 November 2026 I sent the same 15 emoji through the web versions of Discord, Telegram, WhatsApp and Slack, copied each message back with Cmd+C, and diffed the clipboard against the original.
| App | Copied back | Changed |
|---|---|---|
| Discord | Identical | Nothing |
| Telegram | Identical | Nothing |
| 2 of 15 changed | U+FE0F added to bare ❤ and ☺ | |
| Slack | 13 of 15 became shortcodes |
:+1::skin-tone-4:, :rainbow-flag:, … |
The test set
Each case targets one mechanism: a skin-tone modifier (1F44D 1F3FD), ZWJ sequences (1F469 200D 1F4BB, the 7-code-point family), the rainbow flag (1F3F3 FE0F 200D 1F308), regional-indicator and tag-sequence flags, ❤ with and without FE0F, a keycap (0031 FE0F 20E3), two skin tones in one ZWJ sequence, the Emoji 15.1 direction sequence, one emoji each from Emoji 16.0, 17.0 and 18.0, and the text-style ☺ (263A).
Paste, don't type
My first attempt typed the string into Discord character by character, and the composer's emoji autocomplete grabbed partial sequences, so :thumbsup: appeared and the skin tone ended up separate. That's an artifact of synthetic typing, not real use. Simulating a paste fixed it:
const dt = new DataTransfer();
dt.setData('text/plain', message);
editor.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
Reading it back
Rendering doesn't tell you what someone gets when they copy. Discord, WhatsApp and Slack draw emoji as <img> elements, and what lands on the clipboard depends on each app's copy handler. So I selected the sent message, pressed a real Cmd+C (a synthetic execCommand('copy') is blocked without a user gesture) and read the clipboard with pbpaste, then diffed code points:
A = sent.split(' | '); B = copied.split(' | ')
for a, b in zip(A, B):
if a != b:
print(a[:3], [hex(ord(c)) for c in a[4:]], '=>', [hex(ord(c)) for c in b[4:]])
Slack: shortcodes on the clipboard
Slack stores emoji by name and rebuilds the image from the name. Its copy handler writes the name, so this is what came back:
T01 :+1::skin-tone-4: | T02 :female-technologist: | T03 :man-woman-girl-boy: | ...
Paste that into an email or a doc and the reader sees words. The bare heart became :heart: and the bare smiley became :relaxed:, both drawn as their colour (FE0F) versions. The only two emoji copied as real characters were the Emoji 17.0 and 18.0 ones, because Slack doesn't know them yet.
WhatsApp: U+FE0F appears
U+2764 and U+263A default to text presentation; U+FE0F requests emoji presentation. WhatsApp Web added FE0F to both, so the copied string is one code point longer per symbol. That's harmless for reading, and a surprise if you compare strings or count characters.
Discord and Telegram: byte-identical
Both returned all 15 exactly. Discord draws 11 of them with its own SVGs. The bare heart, the bare smiley and the Emoji 17/18 characters fall back to the system font, and on an OS without Emoji 17/18 that's an empty box. Telegram Web uses the system font for everything.
Takeaways
- If your app ingests text pasted from Slack, expect
:shortcode:strings and map them back if you need the characters. - Normalize or strip
U+FE0Fbefore comparing strings that may have passed through WhatsApp. - Brand-new emoji survived transport in all four apps, but three of them (Discord, WhatsApp, Slack) had no artwork for Emoji 17 and 18 yet.
Web clients only, one day, one account per app. Mobile apps may differ. The test cases, raw results and the exact copied text from each app are published with the full write-up on EmojiDig.
Top comments (0)