How to truncate a string with emoji in JavaScript
Use truncate(str, maxLength) from graphemely. It counts grapheme clusters, the characters a person actually sees, so it never cuts an emoji, a flag, a skin tone or an accented letter in half. The ellipsis counts toward the limit, so the result always fits.
The fix, copy and paste
import { truncate } from '@kwy404/graphemely';
truncate('Hello world', 8); // 'Hello w…'
truncate('Hello world', 8, { wordBoundary: true }); // 'Hello…'
truncate('Hello world', 8, { ellipsis: '...' }); // 'Hello...'
const str = 'Hi' + '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}' + '!'; // a family emoji between two words
truncate(str, 3); // 'Hi…', the family is dropped whole
Emoji in the code samples are written as JavaScript escapes, for example \u{1F468} for a man. In your own code you can type the emoji directly.
Why str.slice(0, n) + '...' breaks emoji
JavaScript strings are sequences of UTF-16 code units. slice, substring and substr count code units, but most emoji take two code units (a surrogate pair), and many are built from several code points: a family of three is 8 code units, a flag is 4, a thumbs up with a skin tone is 4. Cutting at an arbitrary code unit index lands inside those sequences.
const str = 'Hi' + family + '!';
str.slice(0, 3) + '...';
// 'Hi\uD83D...' half of an emoji
str.slice(0, 5) + '...';
// 'Hi' + man + ZWJ + '...' one lonely person
import { truncate } from '@kwy404/graphemely';
truncate(str, 3);
// 'Hi…'
truncate(str, 4);
// unchanged, it already fits
The leftover \uD83D is a lone surrogate. It renders as a replacement box, makes encodeURIComponent throw a URIError, and can be rejected by APIs that validate UTF-8.
Options
| Option | Default | What it does |
|---|---|---|
ellipsis | '…' | Text appended when the string is cut. A single character by default. Counts toward maxLength. |
wordBoundary | false | Cut at the last space that fits instead of in the middle of a word, and drop trailing spaces. Falls back to a hard cut for one very long word. |
If the ellipsis does not leave room for any text, the string is cut to maxLength graphemes without an ellipsis. A negative or NaN limit throws a RangeError.
Without a library
Modern runtimes have Intl.Segmenter, which graphemely uses under the hood. The minimal hand written version looks like this:
function truncate(str, max, ellipsis = '…') {
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
const parts = Array.from(segmenter.segment(str), (s) => s.segment);
if (parts.length <= max) return str;
return parts.slice(0, max - 1).join('') + ellipsis;
}
It works, but it does not handle runtimes without Intl.Segmenter, word boundaries, multi character ellipses or invalid input. graphemely does, in about 1.3 kB gzipped when you only import truncate.
Install
npm install @kwy404/graphemely
pnpm add @kwy404/graphemely
yarn add @kwy404/graphemely
bun add @kwy404/graphemely
Works in Node 16 and newer, all modern browsers, Deno and Bun. ESM and CommonJS, TypeScript types included, zero dependencies, about 2 kB gzipped. Try your own text in the playground.
FAQ
Use truncate(str, maxLength) from graphemely. It counts grapheme clusters instead of UTF-16 code units, so an emoji is never cut in half.
Yes. truncate(str, 10) returns at most 10 visible characters including the ellipsis, so the result always fits the space you have.
lodash uses a regular expression that approximates grapheme clusters. It handles common emoji but splits Hangul syllables written with conjoining jamo, counts the England flag as 7 characters and Thai SARA AM as a separate character.
Yes, pass { wordBoundary: true }. graphemely cuts at the last whitespace that fits and removes trailing spaces before the ellipsis.
Related guides
String length with emoji
Why an emoji has length 2, and how to count what users see.
Reverse a string with Unicode
Reverse text without scrambling flags, accents or emoji.
Split a string into characters
Get the visible characters, not code units or code points.
Substring without breaking emoji
A Unicode safe substring and slice with negative indices.
Remove lone surrogates
Detect and repair the broken halves of emoji.