Unicode safe strings for JavaScript and TypeScript
Truncate, slice and count strings with emoji safely in JavaScript
graphemely counts and cuts text by grapheme cluster, the characters people actually see. String length with emoji, truncate with an ellipsis, Unicode safe substring, reverse, pad and wrap, without ever splitting an emoji, a flag, a skin tone or an accent.
npm install @kwy404/graphemely
One import
Every string operation, by visible character
Drop in replacements for the native methods that count UTF-16 code units. Same names, same arguments, the result a person expects.
lengthcounts what people seetruncatenever cuts half an emojisliceandataccept negative indicesreverse,padStartandwrapkeep characters whole
import { length, truncate, slice, reverse, wrap } from '@kwy404/graphemely';
const dev = '\u{1F469}\u{1F3FD}\u{200D}\u{1F4BB}'; // technologist
const title = `Launch day ${dev} is here`;
title.length; // 26 UTF-16 code units
length(title); // 20, what people see
truncate(title, 13); // 'Launch day ' + dev + '…'
truncate('Launch day is here', 13, { wordBoundary: true });
// 'Launch day…'
const flag = '\u{1F1E7}\u{1F1F7}'; // Brazil
slice('Ol\u{E1} ' + flag, -1); // the flag, whole
reverse('Ol\u{E1} ' + flag); // flag + ' álO'
wrap('A quick brown fox', 9); // ['A quick', 'brown fox']
The problem
The problem in four lines
JavaScript strings count UTF-16 code units. Emoji, flags and accents take several, so native methods cut them apart.
const family = '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}';
family.length; // 8
('Hi' + family).slice(0, 3); // 'Hi\uD83D', broken
'cafe\u{301}'.split(''); // 5 items, accent detached
import { length, slice, graphemes } from '@kwy404/graphemely';
length(family); // 1
slice('Hi' + family, 0, 3); // 'Hi' + family
graphemes('cafe\u{301}'); // 4 items
The family emoji above is a man, a woman and a girl joined by zero width joiners: 8 UTF-16 code units, 5 code points, 1 visible character.
Playground: native vs graphemely
Type or paste any text, including emoji, and every result updates as you type. Each operation shows plain JavaScript next to graphemely, running in your browser with the same build that ships on npm.
UTF-16 code units, code points, graphemes
Length
str.length
length(str)
what you see
Slice
whole characters
Truncate
whole characters
Reverse
str.split('').reverse().join('')
reverse(str)
characters intact
Split into characters
str.split('')
graphemes(str)
How it works
Segment first, then count and cut
Segment
Text is split into grapheme clusters with
Intl.Segmenter, or a built-in Unicode UAX #29 fallback where it is missing.Count and cut
Lengths and indices refer to clusters, so a flag, a skin tone or an accented letter is always one unit.
Return whole text
Every result is made of complete characters: no lone surrogates, no detached accents, no half emoji.
Features
Small, complete and safe
Real length
Count graphemes, not UTF-16 code units.
Truncate
Ellipsis and word boundaries, never half an emoji.
Slice and at
Unicode safe substring with negative indices.
Reverse
Flags, skin tones and accents stay intact.
Split
Split a string into the characters users see.
Well formed
Detect and repair lone surrogates.
Pad and wrap
Fixed width text and word wrap by grapheme.
ESLint plugin
Find split(''), unsafe reverse and hand made truncation.
Tiny
About 2 kB gzipped, tree shakeable, zero dependencies.
Install
ESM and CommonJS, TypeScript types included. Works in Node 16 and newer, every modern browser, Deno and Bun.
npm install @kwy404/graphemely
pnpm add @kwy404/graphemely
yarn add @kwy404/graphemely
bun add @kwy404/graphemely
Import
Import only what you use; everything is tree shakeable.
import { truncate, length, slice, reverse } from '@kwy404/graphemely';
// CommonJS
const { truncate } = require('@kwy404/graphemely');
// Deno
import { truncate } from 'npm:@kwy404/graphemely';
API
| Function | Returns | Replaces |
|---|---|---|
graphemes(str) | string[] | str.split(''), [...str] |
length(str), count(str) | number | str.length |
slice(str, start?, end?) | string | str.slice, str.substring |
at(str, index) | string | undefined | str.at, str[i] |
splitAt(str, index) | [string, string] | two slices |
truncate(str, max, { ellipsis, wordBoundary }?) | string | str.slice(0, n) + '...' |
reverse(str) | string | str.split('').reverse().join('') |
padStart(str, n, pad?), padEnd(str, n, pad?) | string | str.padStart, str.padEnd |
wrap(str, width, { hard }?) | string[] | hand written word wrap |
isWellFormed(str), toWellFormed(str) | boolean, string | ES2024 methods, everywhere |
Full reference with examples in the README.
ESLint plugin
Find unsafe string code in an existing project. Flat config, ESLint 8.57 or newer.
- no-unsafe-string-split: reports
str.split('') - no-unsafe-reverse: reports
str.split('').reverse()and[...str].reverse().join('') - prefer-graphemely-truncate: reports
str.slice(0, n) + '...'
import graphemely from '@kwy404/graphemely/eslint-plugin';
export default [graphemely.configs.recommended];
Guides
Fix the common string bugs
Truncate a string with emoji
Cut text to a length with an ellipsis, never half an emoji.
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.
FAQ
Questions
The Unicode name for a character as a person sees it: one or more code points shown as a single unit, such as a letter with an accent, a flag or an emoji with a skin tone.
JavaScript strings are UTF-16. Most emoji live outside the Basic Multilingual Plane and take a surrogate pair of two code units, and .length counts code units. Sequences such as families and flags take even more.
graphemely uses Intl.Segmenter when it exists and adds the operations you actually need on top of it: length, slice, at, truncate with ellipsis and word boundaries, reverse, pad and wrap, plus a Unicode fallback for runtimes without it.
Yes. truncate(str, 10) returns at most 10 visible characters including the ellipsis, so the result always fits the space you have.
Yes. The package ships ESM and CommonJS builds with TypeScript types, has zero dependencies and runs in Node 16 and newer, every modern browser, Deno and Bun.
Browser and runtime support
graphemely uses Intl.Segmenter in Node 16+, Chrome and Edge 87+, Safari 14.1+, Firefox 125+, Deno and Bun, and a built-in Unicode UAX #29 fallback everywhere else. Output is ES2020.