There are three levels, and each needs a different tool:
| Level | Example | How to iterate |
|---|---|---|
| UTF-16 code unit | 0xD83D |
index loop, split("") |
| Code point | U+1F600 | for...of, spread, Array.from, codePointAt |
| Grapheme cluster | ๐จโ๐ฉโ๐ง (5 code points) |
Intl.Segmenter |
The string iterator in ECMA-262 yields one code point at a time, so surrogate pairs stay together. That is enough for a plain emoji such as ๐.
It is not enough for everything a user sees as one character. Unicode UAX #29 defines grapheme clusters as user-perceived characters. Examples that are several code points:
e+ U+0301 (combining acute accent), which renders asรฉ.- A flag such as
๐ฎ๐ณ: two regional-indicator code points. - A family emoji
๐จโ๐ฉโ๐ง: three emoji joined by zero-width joiners (U+200D). - An emoji with a skin-tone modifier,
๐๐ฝ.
Intl.Segmenter (ECMA-402) implements these boundaries. Create it with granularity: "grapheme", call segment(text), and iterate: each item is { segment, index, input }, where index is the UTF-16 offset. Use it for truncation, caret movement, character counters, and reversing text. The default granularity is "grapheme". "word" adds isWordLike, and "sentence" splits sentences.
Boundaries come from the engine's Unicode data (ICU in V8), so results can change when Unicode or the engine updates. Don't persist counts as exact data.