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
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.lengthcounts 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].lengthcounts code points;Intl.Segmentercounts 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 intoslice/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
- String: length — JavaScript | MDN — definition that
lengthcounts UTF-16 code units; source of the emoji, Adlam, and formula examples and the iterator-based character-count pattern. - Intl.Segmenter.prototype.segment() — JavaScript | MDN — grapheme segmentation API and its April 2024 Baseline availability.
- UAX #29: Unicode Text Segmentation — the normative rules defining grapheme cluster (user-perceived character) boundaries used by
Intl.Segmenter. - Unicode Emoji (UTS #51) — specification for skin-tone modifiers, ZWJ sequences, and Regional Indicator flags.
Related on iKit
- Count Chinese, Japanese and Korean words correctly — the sibling problem:
str.lengthalso miscounts extended Han characters, and the sameIntl.Segmenterfix applies. - Word Counter online: live word and character counts — how the in-browser counter reports characters, and why its number can differ from
str.length. - X's 280-character limit: fit more into every tweet — a real place the code-unit-vs-grapheme gap bites when you're packing a message to a hard limit.
- SMS 160-character limit explained — emoji don't just count differently, they can switch SMS encoding and blow the limit early.
- SEO title and meta description length — another character-budget task where counting the wrong unit costs you rendered pixels in search results.
Related posts
WCAG Contrast Ratios: How AA and AAA Are Calculated (2026)
A developer's guide to the WCAG contrast ratio formula, the 4.5:1 AA and 7:1 AAA thresholds, large-text exceptions, and how to check color contrast in code.
OKLCH in CSS: A Practical Guide for Developers (2026)
OKLCH in CSS gives you perceptually uniform color, wider P3 gamut, and predictable lightness. Here is the syntax, the math, and the fallbacks for 2026.
RGB to HEX: The 2026 Guide for Designers and Developers
RGB to HEX is base-10 to base-16 on three bytes. Here's the exact math, the JavaScript one-liners, alpha channels, and the mistakes that trip people up.