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 (1)
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.