Introduction
If you've ever written this in JavaScript:
console.log("π£".length); // 2 π€
or
console.log("π¨βπ©βπ§βπ¦".length); // 11 π±
you've probably wondered:
"How can one visible emoji have a length of 11?"
This article explains everything you need to knowβfrom Unicode to UTF encodings, surrogate pairs, grapheme clusters, and the correct way to count characters in JavaScript.
Unicode: The Universal Character Set
Unicode is not an encoding.
Unicode is simply a standard that assigns every character a unique number called a code point.
Examples:
| Character | Unicode Code Point |
|---|---|
| A | U+0041 |
| δ½ | U+4F60 |
| π | U+1F600 |
| π£ | U+1F423 |
Think of Unicode as a giant dictionary.
A -> U+0041
B -> U+0042
δ½ -> U+4F60
π£ -> U+1F423
Notice Unicode only defines the number.
It does not define how those numbers are stored in memory.
UTF-8, UTF-16 and UTF-32
UTF means Unicode Transformation Format.
Each UTF is simply a different way of encoding Unicode code points.
| Character | Unicode | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|---|
| A | U+0041 | 1 byte | 2 bytes | 4 bytes |
| δ½ | U+4F60 | 3 bytes | 2 bytes | 4 bytes |
| π£ | U+1F423 | 4 bytes | 4 bytes (2 code units) | 4 bytes |
UTF-8
UTF-8 uses between 1 and 4 bytes.
Advantages:
- Most efficient for English text
- Backward compatible with ASCII
- Standard for the web
- Used by JSON, HTML, Linux, HTTP...
Today, UTF-8 dominates the internet.
UTF-16
UTF-16 stores data in 16-bit code units.
Characters inside the Basic Multilingual Plane (BMP) use one code unit.
Characters outside the BMP use two code units.
JavaScript strings are UTF-16.
That's why:
"ABC".length
// 3
and
"δ½ ".length
// 1
work as expected.
UTF-32
UTF-32 stores every Unicode code point using exactly 4 bytes.
Advantages:
- Very simple indexing
- Every code point occupies one slot
Disadvantages:
- Huge memory usage
- 4Γ larger than UTF-8 for English text
Because of this, UTF-32 is rarely used for general-purpose applications.
Why Emoji Break length
Consider:
"π£"
Unicode:
U+1F423
This value is larger than 0xFFFF.
A single UTF-16 code unit can only represent:
0x0000
...
0xFFFF
So UTF-16 needs two code units.
Surrogate Pairs
Unicode reserved two special ranges:
High Surrogates
D800 - DBFF
Low Surrogates
DC00 - DFFF
An emoji becomes:
π£
β
D83D DC23
Therefore JavaScript stores:
[D83D][DC23]
So:
"π£".length
// 2
because JavaScript counts UTF-16 code units.
Why for...of Works Better
Strings are iterable.
for (const ch of "π£") {
console.log(ch);
}
prints
π£
not
D83D
DC23
The iterator automatically detects surrogate pairs.
Therefore
[..."π£"].length
// 1
works.
But for...of Still Isn't Correct
Now look at this emoji:
π¨βπ©βπ§βπ¦
Visually:
π¨βπ©βπ§βπ¦
Internally:
π¨
ZWJ
π©
ZWJ
π§
ZWJ
π¦
There are 7 Unicode code points.
So
[..."π¨βπ©βπ§βπ¦"].length
// 7
Still wrong.
What is ZWJ?
ZWJ stands for Zero Width Joiner.
Unicode:
U+200D
It is an invisible character.
Its job is:
Join the surrounding characters into one displayed glyph.
Example:
Without ZWJ:
π© π»
With ZWJ:
π©βπ»
Internally:
π©
+
ZWJ
+
π»
The rendering engine displays:
π©βπ»
as a single emoji.
Grapheme Clusters
Humans don't think in Unicode code points.
We think in visible characters.
Unicode calls those grapheme clusters.
Examples:
| Text | Code Points | Visible Characters |
|---|---|---|
| A | 1 | 1 |
| δ½ | 1 | 1 |
| π£ | 1 | 1 |
| π»π³ | 2 | 1 |
| ππ½ | 2 | 1 |
| eΜ | 2 | 1 |
| π¨βπ©βπ§βπ¦ | 7 | 1 |
This is what users expect when they see:
Maximum 100 characters
How Does JavaScript Know?
Modern JavaScript provides
Intl.Segmenter
It implements the Unicode Text Segmentation algorithm (UAX #29).
Instead of asking:
"How many UTF-16 code units?"
or
"How many Unicode code points?"
it asks:
"Where are the grapheme boundaries?"
Unicode maintains a database of character properties.
Examples:
π¨
Extended_Pictographic
ZWJ
Zero Width Joiner
π½
Extend
π»
Regional Indicator
The algorithm walks through the string and decides whether each boundary should be broken.
Very roughly:
Read next code point
β
Look up Unicode properties
β
Apply Unicode rules
β
Break?
or
Continue current grapheme?
No machine learning.
No AI.
Just deterministic Unicode rules.
The Right Way to Count Characters
const segmenter = new Intl.Segmenter(undefined, {
granularity: "grapheme"
});
function characterCount(str) {
let count = 0;
for (const _ of segmenter.segment(str)) {
count++;
}
return count;
}
Examples:
characterCount("Hello")
// 5
characterCount("δ½ ε₯½")
// 2
characterCount("π£")
// 1
characterCount("π»π³")
// 1
characterCount("ππ½")
// 1
characterCount("π¨βπ©βπ§βπ¦")
// 1
characterCount("eΜ")
// 1
This works correctly across languages, emoji, combining marks, flags, and ZWJ sequences.
Comparison
| Method | Counts | Correct for Emoji | Correct for Flags | Correct for ZWJ | Correct for Combining Marks |
|---|---|---|---|---|---|
str.length |
UTF-16 code units | β | β | β | β |
for...of / Array.from()
|
Unicode code points | β | β | β | β |
Intl.Segmenter |
Grapheme clusters | β | β | β | β |
Key Takeaways
- Unicode defines characters.
- UTF-8, UTF-16, and UTF-32 are different encodings of Unicode.
- JavaScript strings are UTF-16.
-
String.lengthcounts UTF-16 code units, not characters. -
for...ofiterates Unicode code points, which is better but still not enough. - User-visible characters are called grapheme clusters.
-
Intl.Segmenteris the correct solution for counting characters in modern JavaScript.
If you're building user-facing features like character limits, text editors, or social apps, Intl.Segmenter is the API you should be using.
Top comments (2)
The
lengthproperty lying about Unicode characters is a common gotcha, but what's the best way to accurately count characters in JavaScript, especially with surrogate pairs? I'd love to swap ideas on this.the recommendation is using Intl.Segmenter to count characters based on Unicode grapheme cluster segmentation algorithm. Readmore: developer.mozilla.org/en-US/docs/W...