Short answer: Grapheme clusters approximate user-perceived characters. Code points are Unicode coded values. JavaScript string length counts 16-bit UTF-16 code units. UTF-8 byte length measures encoded storage or transport. The totals can all differ without contradiction. Name the unit, normalization policy, segmentation version, and malformed-input behavior whenever a limit must be reproducible.
Four length units answer four different questions
The word “character” is overloaded. A product designer may mean one thing a user can move over with an arrow key. A Unicode algorithm may operate on code points. JavaScript indexing exposes UTF-16 code units. A database or HTTP request ultimately stores bytes. Treating these layers as interchangeable causes broken emoji, rejected payloads, incorrect progress meters, and security-sensitive truncation mistakes.
| Unit | Question it answers | Typical use |
|---|---|---|
| Grapheme cluster | How many user-perceived text elements? | Cursor movement and visible-character limits |
| Unicode code point | How many coded values are in the sequence? | Unicode property processing and scalar iteration |
| UTF-16 code unit | How many 16-bit elements does the ECMAScript string contain? | JavaScript length and code-unit indexes |
| UTF-8 byte | How large is the encoded scalar-value sequence? | Files, request bodies, buffers, and byte ceilings |
There is no universally “real” total independent of purpose. A user-facing editor should usually avoid splitting a grapheme, but a storage service must still enforce its byte capacity. A JavaScript API may document offsets in code units even when its UI displays grapheme counts. Robust systems keep the mapping explicit.
Grapheme clusters approximate visible characters
Unicode Standard Annex #29 defines default extended grapheme cluster boundaries. The rules keep many combining marks with their base, pair regional indicators for flags, attach emoji modifiers, and preserve recognized extended-pictographic sequences connected by zero-width joiners. The result is designed to be useful for general text editing and boundary movement.
“Approximate” matters. A grapheme cluster is not the same as a glyph. A font can shape multiple clusters together, render one cluster with several glyphs, show a missing-character box, or give a cluster zero or unusual width. Grapheme count does not predict pixels, terminal columns, speech duration, or semantic concepts.
Browser code can request grapheme boundaries with Intl.Segmenter. ECMA-402 defines locale negotiation and the segmentation API; the engine supplies Unicode and locale data. Results on new or obscure sequences may change when engines adopt a later Unicode release, so an audited limit should record the runtime as well as the algorithm name.
Code points are values, not guaranteed visible units
A Unicode code point is written as U+ followed by hexadecimal digits. Latin small e is U+0065, combining acute accent is U+0301, and grinning face is U+1F600. Some code points are letters or symbols, some are combining marks or controls, and the surrogate range U+D800–U+DFFF is reserved for UTF-16 mechanics rather than Unicode scalar values.
ECMAScript code-point iteration, such as for...of or Array.from(text), combines a well-formed surrogate pair into one item. It does not combine a base with its accent, a flag's regional indicators, or an emoji ZWJ sequence. Code-point counting is useful when the contract truly concerns Unicode values, but it is not a substitute for grapheme segmentation.
JavaScript length counts UTF-16 code units
The ECMAScript String type is a sequence of 16-bit unsigned integer values. A Basic Multilingual Plane code point outside the surrogate range usually occupies one unit. A supplementary code point from U+10000 through U+10FFFF is represented by a high-surrogate and low-surrogate pair, so it occupies two units.
"A".length // 1 UTF-16 code unit
"😀".length // 2 UTF-16 code units
Array.from("😀").length // 1 code point
[..."👨👩👧👦"].length // 7 code points, often 1 graphemeMethods such as slice, bracket indexing, and many DOM offsets use code-unit positions. Cutting between the two units of a surrogate pair produces an ill-formed string. Cutting between code points can still split a combining sequence or joined emoji. Convert the desired grapheme boundary back to code-unit offsets before slicing a user-facing string.
UTF-8 measures the encoded payload
UTF-8 encodes each Unicode scalar value in one to four bytes. ASCII-range values use one byte, values through U+07FF use two, most Basic Multilingual Plane values use three, and supplementary values use four. A grapheme containing several code points is the concatenation of all of their encodings, so one visible family emoji can consume far more than four bytes.
| Text | Graphemes | Code points | UTF-16 units | UTF-8 bytes |
|---|---|---|---|---|
A | 1 | 1 | 1 | 1 |
é as U+00E9 | 1 | 1 | 1 | 2 |
é as U+0065 U+0301 | 1 | 2 | 2 | 3 |
😀 | 1 | 1 | 2 | 4 |
👨👩👧👦 | 1 | 7 | 11 | 25 |
The table assumes the displayed sequences exactly as listed and current extended grapheme rules. It demonstrates why a 280-grapheme field has no fixed worst-case UTF-8 size unless the accepted repertoire or a separate byte cap is defined. Review ASCII vs Unicode vs UTF-8 for the full path from coded values to bytes and their hex or binary notation.
Normalization can preserve appearance while changing length
Precomposed U+00E9 and decomposed U+0065 U+0301 are canonically equivalent representations of é. They commonly render alike and each forms one extended grapheme cluster, but the first uses one code point and two UTF-8 bytes while the second uses two code points and three bytes.
Unicode normalization can transform strings into NFC, NFD, NFKC, or NFKD. Canonical forms address canonical equivalence; compatibility forms can also change presentation distinctions. Normalization is not a harmless counting trick. It changes code points and bytes, which matters for signatures, hashes, storage keys, source preservation, and protocols that already specify a form.
A counter that promises to measure the input as entered should not normalize silently. A system that requires NFC should normalize at a documented boundary and count the resulting form. Comparing results only makes sense when both sides use the same policy.
Lone surrogates require an explicit error policy
ECMAScript permits a string to contain a high surrogate not followed by a low surrogate, or a low surrogate without its matching high surrogate. This can arise from careless code-unit slicing, malformed interchange, or deliberate construction. The string still has a code-unit length, but the isolated surrogate is not a Unicode scalar value and therefore has no valid direct UTF-8 encoding.
The WHATWG Encoding Standard's TextEncoder interface takes a USVString. Web IDL conversion replaces unmatched surrogates with U+FFFD, and UTF-8 then encodes that replacement character as three bytes. This guarantees a well-formed byte stream, but it is lossy with respect to the original 16-bit sequence.
Applications should choose and disclose one of three approaches: reject ill-formed UTF-16 before encoding, repair it with a visible warning, or deliberately report the bytes of replacement processing. A strict byte counter can return no byte total plus the offending code-unit position. That prevents three replacement bytes from being mistaken for a faithful representation of the original value.
Design limits around the operation users perform
For a profile name or message composer, grapheme clusters often align best with user expectations. The remaining counter should advance once for a combining sequence or joined emoji and truncation should occur only at a cluster boundary. Still, the server must independently validate its actual contract; a grapheme limit does not protect a byte-limited column.
Use UTF-16 code units when compatibility with an existing JavaScript API is the requirement, even if that metric is awkward for users. Use code points when a Unicode protocol explicitly defines scalar-value or code-point length. Use UTF-8 bytes for file, database, queue, and request payload ceilings. Some systems need two simultaneous policies, such as 100 graphemes and 400 UTF-8 bytes.
- Name the unit in product copy and API documentation. Avoid an unqualified “characters” field.
- Specify normalization. State whether limits apply before or after a named form.
- Specify malformed-input behavior. Reject, replace, or repair lone surrogates deliberately.
- Segment before truncating. Preserve grapheme boundaries for user-visible strings.
- Encode before enforcing byte limits. Do not estimate UTF-8 bytes from code-unit length.
- Test representative sequences. Include combining marks, flags, skin tones, ZWJ emoji, variation selectors, and non-Latin scripts.
Segmentation results can evolve without changing the text
Unicode adds characters and refines properties and boundary rules. Browser engines update on their own release schedules. A sequence that an older runtime splits can become a single recognized emoji grapheme in a newer runtime. Locale tailoring and specialized libraries can introduce further differences while remaining grounded in the same standard.
When a decision must be reproduced, store the original string, the requested locale, the segmentation implementation and version, the Unicode data version if available, the normalization form, and the exact unit. When a remote service owns the limit, run final validation there as well. A local counter is excellent for explanation and drafting, but it cannot override an undocumented server policy.
Inspect all four totals together. Count grapheme clusters, code points, UTF-16 code units, and strict UTF-8 bytes, then set a custom grapheme allowance without uploading the text.
Open the Character CounterFrequently asked questions
What is a grapheme cluster?
A grapheme cluster is a sequence that a Unicode boundary algorithm keeps together as an approximation of one user-perceived character. It can contain one code point or several, such as a base letter plus combining marks or a joined emoji sequence.
Why is JavaScript string length different from the visible character count?
JavaScript string length counts UTF-16 code units. A supplementary code point such as U+1F600 uses a two-unit surrogate pair, and one visible grapheme can contain several code points, so the code-unit total can exceed the grapheme total.
Are code points the same as UTF-8 bytes?
No. A code point is an abstract Unicode value, while UTF-8 encodes a scalar value as one to four bytes. A grapheme can contain several code points, so its complete UTF-8 representation can contain many bytes.
Can equivalent-looking text have different lengths?
Yes. A precomposed accented character and a decomposed base-plus-mark sequence can render alike and each form one grapheme while using different numbers of code points and UTF-8 bytes. A normalization policy is needed when that distinction must be removed.
What happens when UTF-8 encoding sees a lone surrogate?
A lone UTF-16 surrogate is not a Unicode scalar value. Web TextEncoder converts it to the replacement character U+FFFD through USVString processing, so a strict counter should disclose the replacement or reject the byte result instead of implying a lossless encoding.
Which unit should a character limit use?
Use grapheme clusters for many user-facing visible-character limits, UTF-16 code units when matching a JavaScript API, code points when a Unicode-value contract says so, and UTF-8 bytes for encoded storage or transport ceilings. State the unit explicitly.