Skip to content

This Emoji Is One Character, Seven Characters and Eleven Characters

You paste something into a field with a 280-character limit and it says you are over by nine, but you counted 271. Neither of you is wrong. There are four different reasonable answers to "how many characters is this", they disagree most on exactly the text people paste, and knowing which one a system uses tells you why it rejected you.

Ganesh Patil·4 min read
Table: One family emoji, counted four different ways.

Six short strings, counted four different ways:

textgraphemescode pointsUTF-16 unitsUTF-8 bytes
ß1112
é(single code point)1112
é(e + combining accent)1223
👍1124
👍🏽 (with skin tone)1248
👨‍👩‍👧‍👦 (family)171125

The family emoji is the extreme case and it is not exotic — it is on every phone keyboard. One thing on screen, eleven "characters" to a JavaScript .length, and twenty-five bytes in a database.

The four counts, and who uses which

Graphemes are what a person means by "character": one visible unit, however many code points went into it. This is what a text cursor moves over and what pressing backspace deletes. It is also the hardest to compute — the rules are a Unicode annex, they change between versions, and most languages need a library.

Code points are Unicode's own units. éwritten as eplus a combining acute is two code points that render as one grapheme. Emoji sequences are several code points joined by an invisible zero-width joiner. Python's len()counts these.

UTF-16 units are what JavaScript's .lengthreturns, and Java's, and C#'s. Anything outside the first 65,536 code points — which is all emoji, and a great deal of CJK — takes two units instead of one. This is why 👍 costs two and the family costs eleven.

Bytes are what storage and network limits count. In UTF-8, ASCII is one byte, Latin accents two, most CJK three, emoji four.

Where this bites

Twitter-style limits. Platforms that advertise a character limit usually count graphemes or code points, and the box in front of you may count UTF-16 units. The number of "you are over the limit" bugs caused by this is large.

Database columns. VARCHAR(255)means 255 bytes in some databases and 255 characters in others, and in MySQL the character interpretation depends on the column's charset — which is why the older utf8charset, which is really three-byte UTF-8, silently rejects emoji while utf8mb4accepts them. A field that worked for years starts failing when someone types a 👍.

Truncation. Cutting a string at byte 100 can land in the middle of a multi-byte character and produce invalid UTF-8 — the classic at the end of a truncated snippet. Cutting at UTF-16 unit 100 can split a surrogate pair, or split a family emoji into a man, a woman and two children.

Passwords and validation. A minimum length in code points and a maximum in bytes can be contradictory for a user typing in a non-Latin script.

The rules that keep you out of trouble

Store bytes, display graphemes, validate in whatever the user sees. A limit the user cannot verify by looking is a limit they will hit by surprise.

Never truncate by byte or by UTF-16 index without checking the boundary. Most languages have a way to iterate graphemes or at least code points; use it. If you truncate for a preview, truncating at a word boundary is better anyway.

Normalize before comparing or deduplicating. caféwritten two ways is two different strings until you normalize them — which is its own problem.

Size database columns for the worst case. If a field can hold user text it can hold emoji, and emoji are four bytes each. A 20-character name field is 80 bytes if you want it to work for everyone.

Counting it yourself

In a browser console:

const s = "👨‍👩‍👧‍👦"
s.length                                   // 11 — UTF-16 units
[...s].length                              // 7  — code points
[...new Intl.Segmenter("en", {granularity:"grapheme"}).segment(s)].length  // 1
new TextEncoder().encode(s).length         // 25 — UTF-8 bytes

Four lines, four different numbers, one emoji. When a system tells you a length you disagree with, one of these is the number it is using, and the gap between them is almost always where the confusion lives.

Why the counts diverge at all

The gap between these numbers is a history of how text encoding was fixed twice.

Unicode originally assumed 65,536 code points would be enough for every script, and 16 bits per character became the design of Java, JavaScript, Windows and several others. When it turned out not to be enough — historic scripts, rare CJK, and later emoji all live above that ceiling — those systems could not widen their character type without breaking every program written for them. The solution was surrogate pairs: encode a high code point as two 16-bit units that are individually meaningless. That is why .lengthreturns 2 for a single emoji, and it is a permanent artifact of a decision made in 1991.

UTF-8 avoided that problem by being variable-width from the start, at the cost of a character taking between one and four bytes. Graphemes then sit on top of everything as the layer that describes what a reader sees, which needs its own rules because combining marks and joiners were always going to produce visible units made of several code points.

So the four numbers are not four opinions about the same thing. They are four genuinely different quantities, each correct for a different purpose, and the only error is using one where another was meant.