iKit
Technical · 10 min read ·

Why str.length Lies About Emoji in JavaScript (2026)

In JavaScript str.length counts UTF-16 code units, so one emoji reports 2 and a family emoji reports 11. Here is how to count emoji characters correctly.

Why str.length Lies About Emoji in JavaScript (2026)

Why str.length Lies About Emoji in JavaScript

Type "😄".length into a console and you'll get 2, not 1. It isn't a bug — str.length counts UTF-16 code units, and most emoji are stored as two of them. That gap between "code units" and "characters you can see" is where truncated bios, off-by-one validation errors, and broken emoji come from. This guide shows exactly why emoji inflate str.length and how to count them correctly.

TL;DR

  • str.length counts UTF-16 code units, not characters; one emoji usually reports 2.
  • Emoji outside the Basic Multilingual Plane are stored as a surrogate pair — two code units.
  • ZWJ emoji make it worse: 👨‍👩‍👧‍👧 reports a length of 11.
  • [...str].length counts code points; Intl.Segmenter counts real characters.
  • For character limits and truncation, count graphemes, not code units.

What str.length actually counts

The single most useful fact about JavaScript strings: they are sequences of 16-bit values. Per MDN, the length property returns "the length of the string in UTF-16 code units" — not the number of characters a human would count. For plain ASCII the two numbers happen to match, which is exactly why the difference stays hidden until an emoji shows up.

Why "😄".length returns 2, not 1

A code unit is one 16-bit value. Characters whose code point is below U+10000 fit in a single code unit, so they measure as 1. The grinning-face emoji is U+1F600 — above that ceiling — so it can't fit in 16 bits. UTF-16 stores it as two code units, and length faithfully counts both:

"😄".length;       // 2
"a".length;        // 1
"文".length;        // 1  (BMP CJK)

The emoji looks like one character on screen, but the string holds two 16-bit values. length is reporting storage, not perception.

UTF-16 code units and the Basic Multilingual Plane

Unicode is divided into planes. The first, the Basic Multilingual Plane (BMP), spans U+0000 to U+FFFF and holds nearly everything you type daily: Latin, Greek, Cyrillic, and the common CJK characters. Every BMP code point is one UTF-16 code unit, so length matches the visible count for that text.

Everything from U+10000 upward lives in the supplementary planes: emoji, mathematical alphanumeric symbols, historic scripts, and rarer Han characters. Those code points need two code units each, and that's where length starts to diverge from what you see.

Surrogate pairs: how one emoji becomes two code units

The two code units that encode a supplementary character are called a surrogate pair. The first is a high surrogate in the range U+D800–U+DBFF; the second is a low surrogate in U+DC00–U+DFFF. Those ranges are reserved and never used for real characters, so decoders can always tell them apart. You can see the pair directly:

"😀".charCodeAt(0).toString(16); // "d83d" (high)
"😀".charCodeAt(1).toString(16); // "de00" (low)
"😀".codePointAt(0).toString(16); // "1f600"

charCodeAt reads one code unit; codePointAt reads a full code point and reassembles the surrogate pair back into U+1F600. Reach for codePointAt whenever you care about the character rather than the byte layout. If you need to match emoji rather than count them, Unicode property escapes like \p{Emoji} are the modern approach — you can try patterns against emoji strings in the regex tester before shipping them.

How to count emoji characters correctly in JavaScript

There are three different "lengths" you might want, and picking the wrong one is the whole problem. Code units (length), code points (iteration), and grapheme clusters (Intl.Segmenter) give three different numbers for the same string.

Counting code points with the spread operator and Array.from

The string iterator is Unicode-aware: it walks code points, not code units, so it keeps a surrogate pair together. Spreading into an array and taking its length gets you the code-point count:

[..."😄"].length;         // 1
Array.from("😄").length;   // 1
"😄".length;              // 2

Array.from(str) and [...str] behave identically here — both use the string iterator. This already fixes simple emoji. MDN's own recommendation is to split with the iterator when you want character counts.

Why [...str].length still miscounts family and skin-tone emoji

Code-point counting solves the surrogate-pair problem but not the multi-code-point one. Many emoji you see as a single glyph are built from several code points glued together. Iterating by code point splits them:

[..."👍🏽"].length;              // 2  (thumbs-up + skin tone)
[..."👩‍💻"].length;              // 3  (woman + ZWJ + laptop)
[..."👨‍👩‍👧‍👧"].length;   // 7  (four people + three joiners)

Each of those renders as one character, yet code-point counting reports 2, 3, and 7. If you're enforcing a character limit or truncating, that's still wrong.

Counting grapheme clusters with Intl.Segmenter

A grapheme cluster is Unicode's term for a user-perceived character — the unit your cursor moves over and backspace deletes as a whole. The rules that define cluster boundaries come from Unicode Annex #29, Text Segmentation. In the browser, Intl.Segmenter with granularity: "grapheme" applies them for you:

function graphemeCount(str) {
  const seg = new Intl.Segmenter(undefined, {
    granularity: "grapheme",
  });
  return [...seg.segment(str)].length;
}

graphemeCount("👨‍👩‍👧‍👧"); // 1
graphemeCount("👍🏽");          // 1

Intl.Segmenter reached Baseline "widely available" in April 2024, so it works across current browsers without a polyfill. This is the count that matches what a person sees.

Why emoji have more than one code point

Emoji aren't a flat list of glyphs. Modern emoji are a small composition system — a base emoji plus modifiers, or several emoji joined together — and each piece is its own code point. That design is what pushes str.length far past the visible count.

Skin-tone modifiers add a second code point

The five skin tones are separate code points (U+1F3FB–U+1F3FF) that attach to a base emoji. Thumbs-up with a medium skin tone is two code points — the hand and the modifier:

[..."👍🏽"].map((c) => c.codePointAt(0).toString(16));
// ["1f44d", "1f3fd"]
"👍🏽".length; // 4  (2 code units each)

So a single skin-toned emoji is 4 code units by length, 2 code points by iteration, and 1 grapheme.

ZWJ sequences: how 👨‍👩‍👧‍👧 becomes 11 code units

The most extreme cases are ZWJ sequences: two or more emoji joined by the zero-width joiner, U+200D. The family emoji is four person emoji stitched with three joiners:

[..."👨‍👩‍👧‍👧"].map((c) => c.codePointAt(0).toString(16));
// ["1f468","200d","1f469","200d","1f467","200d","1f467"]

Count the code units: each person emoji is 2, each joiner is 1, so 2+1+2+1+2+1+2 = 11. That's why "👨‍👩‍👧‍👧".length is 11. Backspace still deletes the whole family in one press, because your editor segments by grapheme cluster — the same boundaries Intl.Segmenter uses.

Flags, keycaps, and other multi-code-point emoji

Flags follow yet another scheme: two Regional Indicator code points. The Japanese flag is the indicators for "J" and "P":

[..."🇯🇵"].map((c) => c.codePointAt(0).toString(16));
// ["1f1ef", "1f1f5"]
"🇯🇵".length; // 4

Keycap emoji (a digit + variation selector + U+20E3) and many symbols carrying a U+FE0F variation selector behave the same way — one glyph, several code points. The lesson generalizes: never assume one emoji equals one anything.

Where the str.length bug actually bites

This isn't trivia. The gap between code units and characters produces real, shippable bugs — usually in exactly the places where character counts matter most.

Character limits: tweets, SMS, and bios

If you validate a "280 character" field with str.length, an emoji-heavy message will be rejected long before the user hits the real limit — or accepted past it, depending on how the receiving platform counts. Consider a short message:

const msg = "I ❤️ JS 👨‍👩‍👧‍👧";
msg.length;            // 19
graphemeCount(msg);    // 8

length says 19; a human counts 8. Platform counting rules vary, which is why the same message can look "over limit" in one client and fine in another. When you're pasting drafts into the word & character counter to check a limit, the grapheme count is the honest number.

Slicing and truncating strings without breaking emoji

slice, substring, and substr all work on code-unit indices. Cut in the middle of a surrogate pair and you get a lone, unpaired surrogate — a broken character:

"😀 hi".slice(0, 1); // "\uD83D" → renders as �

To truncate safely, segment first and join whole graphemes:

function truncate(str, max) {
  const seg = new Intl.Segmenter(undefined, {
    granularity: "grapheme",
  });
  return [...seg.segment(str)]
    .slice(0, max)
    .map((s) => s.segment)
    .join("");
}

This never splits an emoji, a skin-tone modifier, or a combining accent.

Database columns, validation, and off-by-one bugs

The same trap appears server-side. A VARCHAR(20) sized by "characters" can reject or silently truncate a 20-emoji string that measures far more in code units — or in the UTF-8 bytes many databases actually store. (An emoji in a URL query string is percent-encoded to those UTF-8 bytes, which is why a single 😀 becomes %F0%9F%98%80 — four bytes — when you run it through a URL encoder.) Combining marks add to the confusion: a decomposed "café" (with a separate combining accent) is length 5 but only 4 graphemes. Validate against the unit you promised your users, and be explicit about whether that's code units, code points, or graphemes.

Choosing the right length in JavaScript

There's no single correct length — there's the right length for the job.

Code units vs code points vs graphemes

Here's how the three measures compare on the same set of strings.

String .length [...str].length Graphemes
a 1 1 1
😄 2 1 1
👍🏽 4 2 1
👩‍💻 5 3 1
👨‍👩‍👧‍👧 11 7 1

Use each measure where it belongs:

  • str.length (code units) — memory sizing, indexing into slice/charCodeAt, and any interop with APIs that are themselves code-unit based.
  • [...str].length (code points) — when you need Unicode-aware iteration but the payoff of full grapheme segmentation isn't worth it, and you control the input.
  • Intl.Segmenter (graphemes) — anything a human counts: character limits, cursor movement, truncation, and validation of user text.

When str.length is actually fine

If your input is guaranteed BMP-only — say, a numeric code, a hex color, or an ASCII slug — length is exact and fast, and reaching for a segmenter is overkill. The rule of thumb: the moment user-authored text or emoji can enter the field, stop trusting length as a character count. When in doubt, run a sample through the word & character counter and compare it to what length reports.

References

Related on iKit

Related posts