Emoji
Last updated 2026-11-02
- emoji
- code point
- bytes
- utf8mb4
- length
- grapheme cluster
- rendering
- ZWJ
On this page
There is one thing to get right about emoji: they are characters, not images. So they are governed by encoding, length, charset and font rules — and those rules count "one emoji" differently.
A character, not a picture
😀 is a code point (U+1F600) stored in the text like any other character. The coloured face you see is drawn by a font.
Consequences, all of which matter:
- the same text looks different on Apple, Google and Microsoft systems — three different emoji fonts, three designs;
- a screenshot or a comp with an emoji in it will not reproduce elsewhere;
- a font may carry no emoji glyphs at all, in which case the browser falls back to the system emoji font (see Shaping and fallback);
- colour emoji are implemented with bitmap or vector tables (
CBDT,sbix,COLR…), but to your code they are still one character.
Three kinds of "length"
The same string gives three different numbers:
| Question | How | "😀" |
"👨👩👧" |
|---|---|---|---|
| UTF-16 code units | JavaScript .length, HTML maxlength |
2 | 8 |
| code points | [...str].length, Array.from(str).length |
1 | 5 |
| grapheme clusters (what looks like one character) | Intl.Segmenter |
1 | 1 |
The family emoji is man + zero-width joiner + woman + zero-width joiner + girl: five code points, two of which take no width themselves but decide whether the neighbours merge.
'😀'.length // 2 ← code units
'😀'.codePointAt(0) // 128512
[...'😀'].length // 1 ← code points
[...new Intl.Segmenter().segment('👨👩👧')].length // 1 ← grapheme clusters
Before building a character counter (remaining characters, nickname limits), decide which one you mean. Most products want grapheme clusters, while the implementation usually uses .length.
Bytes
In UTF-8 an emoji is usually 4 bytes (everything outside the BMP takes 4):
| Text | Code points | UTF-16 units | UTF-8 bytes |
|---|---|---|---|
A |
1 | 1 | 1 |
中 |
1 | 1 | 3 |
😀 |
1 | 2 | 4 |
👨👩👧 |
5 | 8 | 18 |
new TextEncoder().encode('😀').length // 4
Buffer.byteLength('😀', 'utf8') // 4 (Node)
"Counted in bytes" and "counted in characters" can differ by 4× — most database surprises come from exactly this.
Lengths in a database
Two kinds of problem look alike and have different causes.
One: the charset cannot hold the character. MySQL's utf8 is really utf8mb3 (max 3 bytes per character), so a 4-byte emoji fails outright:
Incorrect string value: '\xF0\x9F\x98\x80' for column 'name' at row 1
That is not "the column is too short" — it is a 3-byte charset. The fix: use utf8mb4 for the database, tables, columns and connection.
Two: the length is counted differently.
VARCHAR(n)counts characters in MySQL (not bytes); PostgreSQL also counts characters; SQLite barely limits;- but "character" there means code point, not grapheme cluster — a family emoji counts as 5;
LENGTH()returns bytes,CHAR_LENGTH()returns characters (in Postgres,OCTET_LENGTH()andLENGTH()):
UsingSELECT LENGTH('😀'), CHAR_LENGTH('😀'); -- 4, 1LENGTH()for business rules blows up immediately on Chinese and emoji;- index prefixes have byte limits: 767 bytes in the old (
COMPACT) row format; with 4 bytes perutf8mb4character that is whereVARCHAR(191)came from.DYNAMICallows 3072 bytes (aboutVARCHAR(768)).
Front end versus back end
The same sentence can be measured three ways:
| Layer | Counts | "😀" |
|---|---|---|
HTML maxlength / JS .length |
UTF-16 code units | 2 |
Database VARCHAR(n) |
code points | 1 |
Validation regex . without the u flag |
code units | 2 |
Which produces "the form says one character left, the server says it is too long". The usual approach:
- count with
[...value].lengthorIntl.Segmenteron the client, not.length; - validate by characters (
CHAR_LENGTH) on the server; normalize('NFC')before storing (see Character properties);- never truncate by bytes — that cuts a surrogate pair in half and yields mojibake.
Rendering: still not an image
Because emoji are characters, how they look is decided by font plus system:
- Apple Color Emoji, Segoe UI Emoji and Noto Color Emoji are three designs; the same code point is drawn differently;
- some characters have two presentations (text and emoji), chosen by variation selectors:
U+FE0Ffor emoji,U+FE0Efor text; - when a system or font lags behind Unicode, newer emoji show as tofu.
So "the emoji in the comp" may become a different shape elsewhere. Not a bug — normal behaviour for a character.
What you can see here
- the character drawer shows any character's code point and the escape spellings in each language (astral characters like
\uD83D\uDE00are easy to get wrong); - coverage usually shows nothing for emoji: ordinary Latin or Chinese fonts carry no emoji glyphs, and the emoji you see on the page come from the system emoji font;
- measuring an emoji in the metrics panel tells you plainly when the font has no glyph for it.
Further reading
- The Unicode Standard: the emoji chapters and
emoji-data.txt - UTS #51 (Unicode Emoji)
- MDN:
Intl.Segmenter,String#codePointAt - This site: Code points, Character properties