anyword API reference
Overview
anyword is a micro text segmenter built entirely on the native Intl.Segmenter API. Four functions, one options object, three granularities. Stable since 1.0 — the public API follows semver.
Naive JS quietly gets text wrong: .length miscounts emoji and accents, .split(" ") finds no words in Chinese or Thai, [...str] rips 👨👩👧👦 into pieces. The browser already knows where the real boundaries are. anyword is the thin wrapper — no rule tables, no locale files, no config.
import { anyword, anywordCount, anywordTruncate } from 'anyword'
anyword("don't stop 世界")
// ["don't", "stop", "世界"]
anyword("👨👩👧 hi", { by: 'grapheme' })
// ["👨👩👧", " ", "h", "i"]
anywordCount("世界 test")
// 2
anywordTruncate("héllo 👨👩👧", 5, { ellipsis: '…' })
// "héllo…" — never cuts an emoji in halfInstall
npm install anyword
# or
pnpm add anyword
# or
yarn add anywordZero dependencies, under 1kb gzipped, ESM and CJS builds with types. Also on JSR. Or take the whole family at once with npm install anyfamily.
anyword()
The main entry point. Pass text, optionally pass options. Returns the segments as plain strings, in order.
anyword(text)
anyword(text, options?)
anyword('hi there') // ["hi", "there"]
anyword('hi there', { by: 'grapheme' }) // ["h","i"," ","t","h","e","r","e"]
anyword("don't stop 世界") // ["don't", "stop", "世界"]
anyword('Hi. Go now!', { by: 'sentence' }) // ["Hi. ", "Go now!"]Word mode drops the segments between words — spaces and punctuation. Set raw: true to keep them, and the pieces join back into the original string.
anyword('hi, there!') // ["hi", "there"]
anyword('hi, there!', { raw: true }) // ["hi", ",", " ", "there", "!"]anywordParts()
Same arguments as anyword(), but returns { segment, index, isWordLike? } instead of plain strings. The offsets point into the original text, so you can highlight or slice without searching again.
import { anywordParts } from 'anyword'
anywordParts('世界 test')
// [
// { segment: '世界', index: 0, isWordLike: true },
// { segment: 'test', index: 3, isWordLike: true },
// ]
// React: highlight the matched word in place
anywordParts(text, { raw: true }).map((p, i) =>
p.segment === query ? <mark key={i}>{p.segment}</mark> : p.segment,
)isWordLike is present in word mode only — in grapheme and sentence modes every segment is content.
anywordCount()
Takes the same options and counts segments instead of returning them.
anywordCount('世界 test') // 2
anywordCount('世界test') // 2 — .split(/\s+/) says 1
anywordCount('héllo', { by: 'grapheme' }) // 5
anywordCount('👨👩👧', { by: 'grapheme' }) // 1 — "👨👩👧".length is 8Grapheme counting is what a char-limit counter should show: the number of characters the user believes they typed.
anywordTruncate()
anywordTruncate(text, limit, options?) cuts to at most limit segments — graphemes by default, so an emoji or an accented letter is never split.
anywordTruncate('héllo 👨👩👧', 6) // "héllo "
anywordTruncate('héllo 👨👩👧', 5, { ellipsis: '…' }) // "héllo…"
anywordTruncate('one two three', 2, { by: 'word' }) // "one two "
anywordTruncate('short', 99) // "short" — already fitsThe cut lands on a segment boundary and keeps everything before it verbatim, trailing whitespace included. With ellipsis, that whitespace is trimmed and the ellipsis appended — and only when the text was actually too long, so short input comes back untouched. The ellipsis itself does not count toward limit.
Throws RangeError if limit is negative or not finite.
Granularity
by maps straight to Intl.Segmenter.
"word"words (default)"don't stop 世界" → ["don't", "stop", "世界"]"grapheme"user-perceived characters"👨👩👧 hi" → ["👨👩👧", " ", "h", "i"]"sentence"sentences"Hi. Go now!" → ["Hi. ", "Go now!"]Grapheme and sentence modes never drop anything, so raw does nothing there.
Options
by'word' | 'grapheme' | 'sentence'default: 'word'Segmentation unit. anywordTruncate defaults to 'grapheme' instead — cutting by character is what a length limit almost always means.
localestring | string[]default: runtime localeAny valid BCP 47 locale tag, or a fallback array — 'en', 'ja', 'th', ['xx-Nope', 'en'].
rawbooleandefault: falseWord mode only: keep the segments between words — spaces and punctuation. Ignored for grapheme and sentence, which never drop anything.
ellipsisstringdefault: ''anywordTruncate only. Appended when the text was actually cut; trailing whitespace is trimmed first. Does not count toward the limit.
Recipes
Copy, paste, move on.
// Word counter
anywordCount(post.body)
// 412
// Character counter users agree with (👨👩👧 counts as 1, not 8)
anywordCount(input, { by: 'grapheme' })
// Safe preview / char-limit cut
anywordTruncate(bio, 140, { ellipsis: '…' })
// Word-limited excerpt
anywordTruncate(article, 30, { by: 'word', ellipsis: ' …' })
// Per-character animation, emoji intact
anyword(title, { by: 'grapheme' }).map((c, i) => <span key={i}>{c}</span>)
// Safe reverse
anyword(text, { by: 'grapheme' }).reverse().join('')
// Initials
anyword(fullName).slice(0, 2)
.map((w) => anyword(w, { by: 'grapheme' })[0])
.join('')
// Split into sentences
anyword(text, { by: 'sentence' })SSR
anyword is pure and synchronous — no clock, no state — so it renders the same on server and client. Pass a locale to keep output stable across the hydration boundary regardless of the runtime default.
import { anywordCount } from 'anyword'
export function CharCounter({ value }: { value: string }) {
return (
<span>{anywordCount(value, { by: 'grapheme', locale: 'en' })}/280</span>
)
}Locales
Pass any valid BCP 47 tag. Fallback arrays also work. The locale matters most for word breaking in scripts without spaces.
anyword('これは日本語です', { locale: 'ja' }) // ["これ", "は", "日本語", "です"]
anyword('สวัสดีชาวโลก', { locale: 'th' }) // ["สวัสดี", "ชาว", "โลก"] — no spaces needed
anyword("don't stop", { locale: 'en' }) // ["don't", "stop"]
anyword('hi', { locale: ['xx-Nope', 'en'] })When omitted, native Intl uses the runtime locale.
Support flag
Intl.Segmenter is missing on older runtimes. There anyword throws a clear error at call time; check the exported supported flag first if you target them.
import { anyword, supported } from 'anyword'
supported ? anyword(text) : text.split(/\s+/)Through the anyfamily meta-package the same flag is exported as anywordSupported, since supportedcollides with anylong's.
Compatibility
anyword uses Intl.Segmenter — supported everywhere modern, and detectable via supported where it is not.
Limitations
A few things worth knowing before you ship:
Boundaries come from the runtime's ICU data
anyword delegates all segmentation to native Intl. Exact segment lists may vary between Node versions, browsers, and OSes — especially for CJK and Thai. Don't assert on exact arrays across environments; test behaviour, not strings.
Not an NLP toolkit
anyword does one thing: boundaries. No stemming, no stop words, no message catalogs, no tokenizer for model input. Reach for a real NLP library or i18n framework when you need those.
Missing on older runtimes
Intl.Segmenter landed late — Firefox 125, Safari 14.1. On engines without it every anyword function throws. Branch on the exported supported flag if you target them.
Word mode drops separators by default
anyword('hi, there!') returns two words — the comma and spaces are gone, so the pieces do not rejoin into the input. Pass raw: true when you need a lossless round trip.