Counting characters in JavaScript: length, Array.from and Intl.Segmenter

A name such as “𠮷” can count as two characters in a form, and a single emoji can use more of a length limit than expected. JavaScript’s length counts a different unit from the characters you see on screen.

When implementing a character counter or shortening a string, first decide what counts as one character. The examples below compare length, Array.from, and Intl.Segmenter, then show how to keep names and emoji intact.

Count the same string in three ways

The examples use Japanese names and the locale "ja". Choose a locale appropriate to the text in your application.

JavaScript strings use UTF-16 code units. “𠮷” represents one code point using two code units, so "𠮷".length is 2. “髙” is in the Basic Multilingual Plane (BMP), so its length is 1.

A code point is a value assigned by Unicode to a character or other element. A grapheme cluster, which approximates a visible character, can contain several code points. A family emoji, for example, combines several emoji with joining characters.

const segmenter = new Intl.Segmenter("ja", {
  granularity: "grapheme"
});

for (const text of ["𠮷", "髙", "😊", "か\u3099", "👨‍👩‍👧‍👦"]) {
  console.log({
    text,
    codeUnits: text.length,
    codePoints: Array.from(text).length,
    graphemes: Array.from(segmenter.segment(text)).length
  });
}

"か\u3099" combines “か” with a combining voiced sound mark. Its internal sequence differs from the single code point “が”.

String length Array.from Grapheme clusters
𠮷 2 1 1
髙 1 1 1
😊 2 1 1
か + combining voiced sound mark 2 2 1
👨‍👩‍👧‍👦 11 7 1

In these examples, Array.from(text) avoids splitting surrogate pairs, but does not count the family emoji as one character. The Japanese article about surrogate pairs in names explains the underlying character representation in more detail.

Count units closer to visible characters

For a counter beside an input field that should treat an emoji or combining sequence as one unit, use Intl.Segmenter with granularity: "grapheme". Creating the segmenter outside the function avoids rebuilding it on every call.

const segmenter = new Intl.Segmenter("ja", {
  granularity: "grapheme"
});

function countCharacters(text) {
  let count = 0;
  for (const part of segmenter.segment(text)) {
    count += 1;
  }
  return count;
}

console.log(countCharacters("𠮷野家")); // 3
console.log(countCharacters("😊👨‍👩‍👧‍👦")); // 2

This function returns the number of grapheme clusters. It does not measure rendered glyphs or font widths. Fitting text into a particular width therefore needs a layout calculation in addition to any character limit.

Shorten text without breaking character sequences

"𠮷野家".slice(0, 1) leaves only half of the first surrogate pair. For a shortened display name, taking whole grapheme clusters is easier to handle than cutting at a code unit offset.

const segmenter = new Intl.Segmenter("ja", {
  granularity: "grapheme"
});

function takeCharacters(text, limit) {
  if (!Number.isInteger(limit) || limit < 0) {
    throw new RangeError("limit must be a non-negative integer");
  }

  let result = "";
  let count = 0;
  for (const { segment } of segmenter.segment(text)) {
    if (count >= limit) break;
    result += segment;
    count += 1;
  }
  return result;
}

console.log(takeCharacters("𠮷野家", 1)); // 𠮷
console.log(takeCharacters("👨‍👩‍👧‍👦と家族", 1)); // 👨‍👩‍👧‍👦

When saving a person’s name, it is better to explain the limit and what needs editing than to silently discard part of the input. Use this shortening function for lists or previews, while preserving the original stored value.

Align limits in the form and the storage layer

HTML’s maxlength also limits UTF-16 code units. Adding a grapheme-based counter does not change the browser’s maxlength unit. For a design that accepts 20 visible characters, test names and emoji to check that the HTML limit and the custom validation agree.

The server needs to use the same counting unit as well. A database column limit or an API byte limit is a separate constraint: accepting 20 characters on screen does not guarantee that the value can be saved. Checking that the database character set can store supplementary-plane characters also helps identify failures.

For systems that support older browsers or WebViews, check typeof Intl.Segmenter === "function". Falling back to Array.from changes the counting unit. Consider a grapheme-aware library if the same behavior must be retained on unsupported clients.

Specification references