# anyword β words & graphemes
> Splits text into words, characters or sentences the way people see them β emoji stay whole, Thai and Chinese split without spaces β and counts or truncates on those boundaries.
- wraps: Intl.Segmenter
- install: npm install anyword
- size: 0.8 kB gzip, zero dependencies, ESM + CJS, TypeScript types included
- runtime: Node 16+, Chrome 87+, Safari 14.1+, Firefox 125+ β branch on anyword.supported for older engines.
- docs: https://anyfamily.site/docs/anyword
- demo: https://anyfamily.site/anyword
- playground: https://stackblitz.com/github/kirilinsky/anyfamily/tree/main/examples/vanilla?file=src/main.ts
- npm: https://www.npmjs.com/package/anyword
- source: https://github.com/kirilinsky/anyfamily/tree/main/packages/anyword
- family: https://anyfamily.site/llms.txt
## why
"π¨βπ©βπ§".length is 8, split(" ") finds one word in a Thai sentence, slice() cuts an emoji in half. anyword hands you the runtime's real boundaries β for counters, previews and highlights β in ~0.8 kB.
## usage
```ts
anyword("don't stop δΈη") // ["don't", "stop", "δΈη"]
anyword("π¨βπ©βπ§ hi", { by: "grapheme" }) // ["π¨βπ©βπ§", " ", "h", "i"]
anyword.count("hΓ©llo", { by: "grapheme" }) // 5 β what a character counter should show
anyword.truncate("hΓ©llo π¨βπ©βπ§", 5, { ellipsis: "β¦" }) // "hΓ©lloβ¦"
anyword.parts("δΈη test") // [{ segment: "δΈη", index: 0, isWordLike: true }, β¦]
anyword.supported // Intl.Segmenter present?
```
## not for
Stemming, tokenising for search, or language detection.
---
# anyword
Micro text segmenter built on native Intl. Split, count and truncate text by word, grapheme or sentence β in any locale.
βΈ live demo Β· βΈ full docs Β· βΈ any family
---
**One export. Correct boundaries. Any locale. Zero dependencies.**
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.
```ts
import { anyword } from "anyword";
anyword("don't stop δΈη"); // ["don't", "stop", "δΈη"]
anyword("π¨βπ©βπ§ hi", { by: "grapheme" }); // ["π¨βπ©βπ§", " ", "h", "i"]
anyword.count("δΈη test"); // 2
anyword.count("π¨βπ©βπ§", { by: "grapheme" }); // 1 β "π¨βπ©βπ§".length is 8
anyword.truncate("hΓ©llo π¨βπ©βπ§", 5, { ellipsis: "β¦" }); // "hΓ©lloβ¦"
```
---
## install
```bash
npm install anyword
```
---
## usage
```ts
anyword(text);
anyword(text, options);
```
Returns the segments as plain strings, in order. Everything else hangs off the
same name:
```ts
anyword.parts(text, options?); // { segment, index, isWordLike? }[]
anyword.count(text, options?); // number
anyword.truncate(text, limit, options?); // string
anyword.supported; // boolean
```
`anyword.parts()` carries offsets into the original text, so you can highlight
or slice without searching again.
```tsx
anyword.parts("δΈη test");
// [{ segment: "δΈη", index: 0, isWordLike: true }, { segment: "test", index: 3, isWordLike: true }]
anyword.parts(text, { raw: true }).map((p, i) =>
p.segment === query ? {p.segment} : p.segment,
);
```
---
## recipes
Copy, paste, move on.
```tsx
// Word counter
anyword.count(post.body);
// 412
// Character counter users agree with (π¨βπ©βπ§ counts as 1, not 8)
anyword.count(input, { by: "grapheme" });
// Safe preview / char-limit cut
anyword.truncate(bio, 140, { ellipsis: "β¦" });
// Word-limited excerpt
anyword.truncate(article, 30, { by: "word", ellipsis: " β¦" });
// Per-character animation, emoji intact
anyword(title, { by: "grapheme" }).map((c, i) => {c});
// 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" });
```
anyword is pure and synchronous β no clock, no state β so it renders the same on
server and client. Pass an explicit `locale` to keep it that way.
---
## granularity
`by` maps straight to `Intl.Segmenter`:
| `by` | Unit | Example |
| --- | --- | --- |
| `"word"` | words (default) | `"don't stop δΈη"` β `["don't", "stop", "δΈη"]` |
| `"grapheme"` | user-perceived characters | `"π¨βπ©βπ§ hi"` β `["π¨βπ©βπ§", " ", "h", "i"]` |
| `"sentence"` | sentences | `"Hi. Go now!"` β `["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.
```ts
anyword("hi, there!"); // ["hi", "there"]
anyword("hi, there!", { raw: true }); // ["hi", ",", " ", "there", "!"]
```
Grapheme and sentence modes never drop anything, so `raw` does nothing there.
β [Every granularity in detail](https://anyfamily.site/docs/anyword#granularity)
---
## options
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `by` | `"word" \| "grapheme" \| "sentence"` | `"word"` | `truncate` defaults to `"grapheme"` |
| `locale` | `string \| string[]` | runtime locale | BCP 47 tag or fallback array |
| `raw` | `boolean` | `false` | word mode only β keep spaces and punctuation |
| `ellipsis` | `string` | `""` | `truncate` only β appended when text was cut |
`truncate` cuts on a segment boundary, so an emoji or an accented letter is
never split. The ellipsis does not count toward the limit, and short input comes
back untouched.
β [What each option does, with examples](https://anyfamily.site/docs/anyword#options)
---
## locales
Any valid BCP 47 tag, and fallback arrays. The locale matters most for word
breaking in scripts without spaces. When omitted, native `Intl` uses the runtime
locale.
```ts
anyword("γγγ―ζ₯ζ¬θͺγ§γ", { locale: "ja" }); // ["γγ", "γ―", "ζ₯ζ¬θͺ", "γ§γ"]
anyword("ΰΈͺΰΈ§ΰΈ±ΰΈͺΰΈΰΈ΅ΰΈΰΈ²ΰΈ§ΰΉΰΈ₯ΰΈ", { locale: "th" }); // ["ΰΈͺΰΈ§ΰΈ±ΰΈͺΰΈΰΈ΅", "ΰΈΰΈ²ΰΈ§", "ΰΉΰΈ₯ΰΈ"] β no spaces needed
anyword("don't stop", { locale: "en" }); // ["don't", "stop"]
```
---
## vs the alternatives
| | anyword | grapheme-splitter | words-count + lodash |
| --- | :---: | :---: | :---: |
| gzip | **< 1kb** | ~10kb | ~25kb |
| unicode data bundled | **no** | yes | yes |
| boundary rules | **native Intl** | bundled tables | regex |
| word / sentence mode | **yes** | grapheme only | spaces only |
| dependencies | **0** | 0 | 1+ |
anyword is not an NLP toolkit β it does one thing. Reach for a tokenizer or a
full i18n framework when you need stemming, stop words or message catalogs.
---
## stability
anyword follows [semver](https://semver.org/). The public API is a single
export β `anyword`, with `parts`, `count`, `truncate` and `supported` on it β
plus `AnywordOptions`, `Granularity` and the exported types. It only changes
shape in a major release.
Segment lists come from the runtime's ICU data and may vary between Node
versions, browsers and OSes β especially for CJK and Thai. Test behaviour, not
exact arrays.
### migrating from 1.x
2.0 removed the separate `anywordParts`, `anywordCount`, `anywordTruncate` and
`supported` exports. All four are the same values, now reached through the one
name the package exports:
```diff
- import { anyword, anywordCount, anywordTruncate, supported } from "anyword";
+ import { anyword } from "anyword";
- anywordCount(text);
+ anyword.count(text);
- anywordTruncate(text, 20);
+ anyword.truncate(text, 20);
- supported ? anyword(text) : text.split(/\s+/);
+ anyword.supported ? anyword(text) : text.split(/\s+/);
```
Arguments, return values and throwing behaviour are unchanged. Every `any*`
package follows this shape from 2.0 on: the bare call does the job, everything
else hangs off the same name.
---
## compatibility
`Intl.Segmenter` landed late β Firefox 125, Safari 14.1. On engines without it
every call throws; branch on `anyword.supported` if you target them.
Node.js 18+ Β· Chrome 87+ Β· Firefox 125+ Β· Safari 14.1+ Β· Edge Runtime Β·
Cloudflare Workers Β· Deno
CI runs the full suite on Node 20, 22 and 24.
---
## the any family
anyword is part of **any family** β tiny, zero-dependency wrappers over native
`Intl`, one API per package.
| | | |
| --- | --- | --- |
| [anywhen](https://anyfamily.site/anywhen) | dates & relative time | `Intl.DateTimeFormat` |
| [anyamount](https://anyfamily.site/anyamount) | numbers, currency, units | `Intl.NumberFormat` |
| [anymany](https://anyfamily.site/anymany) | lists | `Intl.ListFormat` |
| [anyaround](https://anyfamily.site/anyaround) | names & flags | `Intl.DisplayNames` |
| [anylong](https://anyfamily.site/anylong) | durations | `Intl.DurationFormat` |
| [anyplural](https://anyfamily.site/anyplural) | plurals | `Intl.PluralRules` |
| [**anyword**](https://anyfamily.site/anyword) | words & graphemes | `Intl.Segmenter` |
Want all of them? [`anyfamily`](https://www.npmjs.com/package/anyfamily) is one
install for the lot, and [`anyfamily-react`](https://www.npmjs.com/package/anyfamily-react)
wraps each as a hook with a shared locale provider.
```bash
npm install anyfamily
```
---
MIT Β© [kirilinsky](https://github.com/kirilinsky)