CHAPTER 03 / 10

What Counts as One Character? Code Points, Graphemes, and Glyphs

A person sees one emoji; a program reports five units. Two names look identical, yet a direct comparison says they differ. These are not necessarily contradictions. The word “character” hides several distinct concepts. Code units, code points, grapheme clusters, and glyphs each describe a useful part of text, but choosing the wrong one can break limits, previews, and editing behavior.

Copy2Paste9 min read

One string can answer several different counting questions

A code unit is a basic unit of an encoding form. A code point is a position in Unicode's code space. A grapheme cluster is a sequence identified by text-segmentation rules. A glyph is a shape produced through fonts and rendering. These concepts are connected, but they are not interchangeable names for the same object.

A network limit asks about encoded bytes. A JavaScript string method may need an offset in code units. A nickname preview often needs to keep perceived characters intact. A designer wants to know whether a line fits in a box. There is no single length operation that can answer all four questions accurately without knowing the intended task.

This article uses default extended grapheme clusters as a practical approximation to perceived characters. That qualification matters. Languages, editors, and particular operations can require tailoring or more detailed treatment. Grapheme segmentation is a defined engineering tool, not a claim that every writing system shares exactly the same concept of a character. Start by naming the product behavior you need, then choose the level of text processing that supports it.

Why JavaScript length can be larger than expected

JavaScript strings consist of 16-bit elements, and length counts those elements. A and 中 each use one; 😀 uses two. The woman technologist emoji, 👩‍💻, uses five: two UTF-16 code units for the woman, one for the zero-width joiner, and two for the computer. Rendering the sequence as one image does not change that underlying count.

The language is following its contract. An application introduces the mismatch when it presents that number as a count of the characters users believe they entered. A “ten-character nickname” rule based on length can allow ten ordinary letters but only five of our smiley example. Users should not have to understand surrogate pairs to discover what a form's label means.

First identify the source of the restriction. If an external interface truly imposes a code-unit limit, validate that limit and explain it accurately where necessary. If the goal is a readable preview, use segmentation appropriate to that goal. The wording on the form is part of the implementation: promising one unit while counting another is a product bug even when the string function itself works correctly.

Code-point iteration helps, but it is not the last step

JavaScript's string iterator keeps a valid surrogate pair together. Spreading 😀 into an array therefore produces one element. Spreading 👩‍💻 produces three, because the woman, joiner, and computer remain separate code points. This is a useful improvement over blindly taking individual code units, but it does not yet reproduce the reader's single-symbol view.

Imagine a decoration tool that inserts a space between every iterated element. It will avoid splitting the smiley's surrogate pair, yet it can still disrupt a joined emoji sequence. Reversing those elements can change their relationships too. A combining mark may become detached from the letter it was meant to modify. “We switched to code points” is therefore progress, not proof that every operation now preserves user-perceived characters.

When diagnosing the output, describe the failure precisely. Did slicing create an unpaired surrogate? Did valid code points survive in a different order? Did the text remain unchanged while a font failed to form the expected image? A screenshot can make these failures look similar. A code-point listing helps distinguish them and directs the repair to the appropriate layer.

An accented letter exposes the difference clearly

The letter é can be represented as U+00E9 or as U+0065 followed by U+0301, a combining acute accent. The first representation contains one code point; the second contains two. Default grapheme segmentation treats each example as one cluster. A reader may see the same accented letter while a direct comparison of the underlying sequences finds a difference.

Consider a mailing list assembled from two systems. The same person's surname might arrive in these two forms. Comparing raw sequences can leave an apparent duplicate. Removing all accents to force a match creates a different problem: it may merge names that should remain distinct. Recognizing a defined equivalence is not the same operation as deleting meaningful information.

Normalization can help establish a comparison policy, but keep it conceptually separate from segmentation. Segmentation finds boundaries; normalization chooses a representation according to an equivalence rule. Neither automatically decides the complete rules for your search feature. Retaining the original input while deriving a documented comparison key is often easier to audit than silently rewriting names and leaving no record of what changed.

A flag and a profession emoji are sequences

The flag 🇨🇳 contains two regional-indicator code points. The profession emoji 👩‍💻 contains three code points, including a zero-width joiner. In the runtime used for this article, Intl.Segmenter in grapheme mode treats each as one segment. Whether a particular device draws the expected combined image is a separate question about rendering support.

A useful experiment is to copy the same sequence into a text field, a webpage, and the destination application. Compare the copied-back code points before comparing screenshots. If the sequence survives but the appearance changes, deleting a joiner is not an encoding repair. It changes the content so that a particular device displays a different result.

Do not assume that every arbitrary sequence containing a joiner is a standardized emoji, either. Being representable as text, being listed as an emoji sequence, forming one grapheme cluster, and rendering as a combined image are distinct conditions. Tools that report those conditions separately help users understand partial support. A single green “valid” badge can hide more than it explains when systems have different data and font versions.

Use Intl.Segmenter with an explicit granularity

Intl.Segmenter can divide text at grapheme, word, or sentence boundaries. The example explicitly chooses grapheme and counts the resulting segments. All six samples produce one grapheme in the tested environment, while their code-unit and code-point counts differ. Every number describes the same original text; each number answers a different question.

Treat the snippet as an observation tool, not a universal validator. The environment must provide the API, and its Unicode data and segmentation implementation can change with runtime updates. If a product needs agreement across clients, record the relevant dependencies and test the same examples on each supported environment. A matching API name is not evidence that all versions will handle every future sequence identically.

There is also an indexing detail worth remembering. A segment's index identifies its position in the original JavaScript string, using that string's code-unit indexing. It is not the ordinal number of the grapheme cluster. Use your iteration count for a label such as “third character,” and the returned index when locating the segment in the original string. Confusing those two quantities can reintroduce the bug segmentation was meant to prevent.

javascript
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
for (const text of ['A', '中', '😀', 'e\u0301', '👩‍💻', '🇨🇳']) {
  console.log(text, {
    utf16Units: text.length,
    codePoints: [...text].length,
    graphemes: [...segmenter.segment(text)].length
  });
}
//                    UTF-16 units / code points / graphemes
// A                   1 / 1 / 1
// 中                  1 / 1 / 1
// 😀                  2 / 1 / 1
// e + U+0301          2 / 2 / 1
// 👩‍💻                5 / 3 / 1
// 🇨🇳                  4 / 2 / 1

Truncate according to the promise your interface makes

Suppose a nickname preview should show its first six grapheme clusters. Segment the string, take six complete segments, and join their original text. Add an ellipsis only if additional segments remain. Comparing the number of segments with the limit is the appropriate test here; comparing the original UTF-16 length would answer a different question.

That approach preserves the sample combinations in this article, but it does not impose a strict byte budget. A cluster can contain multiple combining marks. A small cluster count therefore does not guarantee a small input. Public-facing tools may need a separate overall resource limit as well as a visible-character limit. Both can be legitimate when their purposes and error messages are clear.

Check truncation with text evidence, not just appearance. Confirm that the retained portion consists of complete segments, that it is the intended prefix, and that copying it does not introduce extra spaces or markers. A font can hide differences that still matter downstream. Full editors may also have language-specific cursor and deletion behavior, so a general preview helper should not be mistaken for a complete model of editing.

One drawn shape does not necessarily mean one character

A font can render the letters f and i as a ligature while default grapheme segmentation still treats them as two clusters. Conversely, rendering one cluster can involve more than a single simple shape. Counting the outlines visible on screen does not reveal a reliable code-point count or an editing boundary. A screenshot alone cannot preserve all the relevant text information.

Regional Han glyph forms, connected Arabic writing, and the positioning of combining marks all illustrate why text layout is more involved than placing independent character pictures side by side. Appropriate language information and fonts matter alongside correct storage. A Unicode inspector can reveal the sequence; it cannot replace checking how that sequence actually reads on the target device.

This also explains the tradeoff between copying text and exporting an image. Text lets the destination render the preserved sequence and usually remains editable and searchable. An image fixes one visual result but does not retain the same text behavior. Choose deliberately for the task. Quietly substituting an image because the current font is disappointing can change what users are able to do with the result.

Equal counts do not establish equal content

Once the counts make sense, avoid assigning them more power than they have. Two different names can contain the same number of grapheme clusters. Letters from different scripts can look similar. A length check is a useful constraint, but it cannot decide identity, deduplication, or the complete validity of a username.

Conversely, different code-point counts can occur in canonically equivalent representations. A search system should specify how it preserves originals, derives comparison keys, and explains matches. Concrete examples make that policy reviewable. “Smart matching” is much less useful when nobody can explain why two records were merged or reproduce the decision from the original inputs.

A good bug report includes the copyable original, code points, relevant counts, and intended operation. “Taking the first three graphemes leaves part of the fourth symbol” is more actionable than “Unicode length is broken.” Another developer can reproduce that sequence without depending on the font in your screenshot. The report then helps separate an error in counting from an error in slicing or a difference in rendering.

Turn Unicode support into testable promises

A compact test set can include ordinary English, a common Han character, a supplementary character, a combining accent, and a joined emoji. Run counting, prefix extraction, copying, saving, and reading back against the same set. Testing only a simple Chinese greeting misses surrogate-pair handling; testing only a smiley misses some multi-code-point combinations.

The examples here were checked with Node.js 22, Unicode 17 data, and ICU 78. Those results describe the established examples in that environment. They do not claim that the runtime contains complete data from a later Unicode release. Standards, runtimes, and fonts update independently. Include versions and exact sequences when reporting unexpected behavior so someone else can reproduce the relevant conditions.

For everyday use, preserve the original before cleaning and inspect the sequence when appearance is ambiguous. For implementation work, label every length by its unit and test the writing systems your audience actually uses. Once “one character” becomes a precise requirement, nickname limits and copied-text surprises stop being mysteries. The Unicode inspector offers a convenient place to compare those counts with your own examples.

References

  1. UTR #17: Unicode Character Encoding Model ↗
  2. UAX #29: Unicode Text Segmentation ↗
  3. ECMAScript: The String Type ↗
  4. Unicode FAQ: Characters and Combining Marks ↗
  5. UAX #15: Unicode Normalization Forms ↗
  6. UTS #51: Unicode Emoji ↗
  7. ECMA-402: Intl.Segmenter ↗
  8. Unicode 18.0, Chapter 2: General Structure ↗
  9. Unicode FAQ: Chinese and Japanese ↗
  10. ECMAScript: String Iterator Objects ↗