Code points vs graphemes vs bytes: why emoji length is 7

Why "🤦🏽‍♂️".length is 7, how many characters a string really has, and how to count, cut and store text with emoji and Vietnamese accents correctly.

On this page

Open a browser console and type "🤦🏽‍♂️".length. The answer is 7, for something that looks like one character. This is not a JavaScript bug. Text has at least four sizes, and most "string length" bugs come from using one where another was meant. This article takes the facepalm emoji and a few Vietnamese words apart, explains each size, and shows how to count, truncate, compare and store text without breaking it. Every number below can be reproduced in the Unicode and emoji lookup.

The four sizes of one emoji

🤦🏽‍♂️ (man facepalming: medium skin tone) is built from five Unicode code points:

Code pointNameUTF-16 unitsUTF-8 bytes
U+1F926FACE PALM24
U+1F3FDEMOJI MODIFIER FITZPATRICK TYPE-424
U+200DZERO WIDTH JOINER13
U+2642MALE SIGN13
U+FE0FVARIATION SELECTOR-1613

So it is 1 grapheme cluster (what you see), 5 code points, 7 UTF-16 code units and 17 UTF-8 bytes. The face and the skin-tone modifier sit above U+FFFF, outside the Basic Multilingual Plane, so UTF-16 stores each as a pair of "surrogate" units; JavaScript, Java and C# strings are UTF-16, so their length reports 7. The zero width joiner glues the face and the male sign into one picture, and the variation selector asks for the colourful emoji style.

A family emoji such as 👨‍👩‍👧‍👦 goes further: 1 grapheme, 7 code points, 11 UTF-16 units and 25 UTF-8 bytes. A flag like 🇻🇳 is two regional indicator letters, V and N: 1 grapheme, 2 code points, 4 units, 8 bytes.

Accents do the same thing

Vietnamese shows that this is not only about emoji. The word "Việt" is 4 graphemes. Written in the usual composed form (NFC), "ệ" is one code point, U+1EC7, and the word is 4 code points, 4 UTF-16 units and 6 UTF-8 bytes. Written in decomposed form (NFD), "ệ" is three code points: e, U+0323 COMBINING DOT BELOW and U+0302 COMBINING CIRCUMFLEX ACCENT. The word still looks identical but is now 6 code points, 6 units and 8 bytes.

That is also why "é" === "é" is false in JavaScript even though both display the same: one is U+00E9, the other is "e" plus a combining acute accent. Text copied from macOS file names, from PDFs or typed with some keyboards often arrives in NFD.

Which size to use

  • Graphemes for anything a person counts: character limits in a form, "show the first 50 characters", cursor movement, reversing a string. In JavaScript, [...new Intl.Segmenter().segment(text)].length counts them; most languages have a library for it.
  • Code points for Unicode properties and for interchange formats that are defined on code points. [...text].length (or Array.from) counts code points in JavaScript.
  • UTF-16 units only for indexes you pass back to the same runtime, such as selectionStart in a text field.
  • UTF-8 bytes for storage and transport: database column sizes, HTTP bodies, file sizes. new TextEncoder().encode(text).length gives them.

Truncating and comparing safely

Cutting by UTF-16 units can split a code point in half. "Hi 👍🏽".slice(0, 4) returns "Hi " followed by a lone high surrogate, which displays as � and can make JSON serialisers or databases reject the string. Cutting by code points avoids that but can still separate the thumbs-up from its skin tone or an "e" from its accent. Cut on grapheme boundaries instead: segment, take the first n graphemes, join.

To compare text, normalise both sides to the same form first: a.normalize("NFC") === b.normalize("NFC"). NFC is the right default for storage. For identifiers, usernames and search keys, NFKC also folds compatibility characters (the ligature fi into "fi", fullwidth letters into normal ones), which prevents two different-looking spellings of the same name.

Common mistakes

  • Validating length with `.length`. A 10-emoji message can be rejected as 40+ "characters", while a database limit in bytes is exceeded by text that passed a .length check.
  • Using MySQL's `utf8` charset. It is a 3-byte encoding (utf8mb3) and cannot store emoji, which need 4 bytes in UTF-8. Use utf8mb4.
  • Reversing or truncating with `split("")`. It splits surrogate pairs and combining marks; reverse "Việt" in NFD and the accents land on the wrong letters.
  • Comparing without normalising. Two identical-looking file names, one NFC and one NFD, are different strings.
  • Assuming one emoji is one code point. Skin tones, gender signs, hair styles and flags are sequences.

Try it

Paste any text into the inspect tab of the Unicode and emoji lookup to see its four counts, each grapheme's code points with names and bytes, and the four normalization forms. For word and character counts of longer documents, the text toolkit counts graphemes too.

Tools mentioned in this article

Back to the blog