graphemely

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.

Terminal
npm install @kwy404/graphemely
  • npm version
  • npm downloads per month
  • About 2 kB gzipped
  • Zero dependencies
Gem, the graphemely cat, playing with a ball of yarn whose thread is cut cleanly

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.

  • length counts what people see
  • truncate never cuts half an emoji
  • slice and at accept negative indices
  • reverse, padStart and wrap keep characters whole
app.js
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.

Native JavaScript
native.js
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
graphemely
graphemely.js
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

Native str.length
graphemely length(str) what you see

Slice

Native
graphemely whole characters

Truncate

Native
graphemely whole characters

Reverse

Native str.split('').reverse().join('')
graphemely reverse(str) characters intact

Split into characters

Native str.split('')
graphemely graphemes(str)

How it works

Segment first, then count and cut

  1. Segment

    Text is split into grapheme clusters with Intl.Segmenter, or a built-in Unicode UAX #29 fallback where it is missing.

  2. Count and cut

    Lengths and indices refer to clusters, so a flag, a skin tone or an accented letter is always one unit.

  3. Return whole text

    Every result is made of complete characters: no lone surrogates, no detached accents, no half emoji.

Install

ESM and CommonJS, TypeScript types included. Works in Node 16 and newer, every modern browser, Deno and Bun.

Terminal
npm install @kwy404/graphemely

Import

Import only what you use; everything is tree shakeable.

index.js
import { truncate, length, slice, reverse } from '@kwy404/graphemely';
// CommonJS
const { truncate } = require('@kwy404/graphemely');
// Deno
import { truncate } from 'npm:@kwy404/graphemely';

API

FunctionReturnsReplaces
graphemes(str)string[]str.split(''), [...str]
length(str), count(str)numberstr.length
slice(str, start?, end?)stringstr.slice, str.substring
at(str, index)string | undefinedstr.at, str[i]
splitAt(str, index)[string, string]two slices
truncate(str, max, { ellipsis, wordBoundary }?)stringstr.slice(0, n) + '...'
reverse(str)stringstr.split('').reverse().join('')
padStart(str, n, pad?), padEnd(str, n, pad?)stringstr.padStart, str.padEnd
wrap(str, width, { hard }?)string[]hand written word wrap
isWellFormed(str), toWellFormed(str)boolean, stringES2024 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.

eslint.config.js
import graphemely from '@kwy404/graphemely/eslint-plugin';

export default [graphemely.configs.recommended];

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.