anymany API reference
Overview
anymany is a list formatter built entirely on the native Intl browser API. One function, one options object. Sorted right, joined right, in any locale. Stable since 1.0 — the public API follows semver.
The browser already knows how to join and collate lists in 200+ languages. anymany just makes that API pleasant to use.
import { anymany } from 'anymany'
anymany(['banana', 'apple', 'cherry'])
// "banana, apple, and cherry"
anymany(['S', 'M', 'L'], { type: 'disjunction' })
// "S, M, or L"
anymany(['x', 'y', 'z', 'a', 'b', 'c', 'd'], { max: 3 })
// "x, y, z, and +4"Install
npm install anymany
# or
pnpm add anymany
# or
yarn add anymanyanymany()
The single entry point. Pass an array of strings, optionally pass options. Non-string items are coerced via String(). An empty array returns ""; a single item is returned as-is.
anymany(items)
anymany(items, options?)
anymany(['read', 'write'])
// "read and write"
anymany(['a', 'b', 'c'], { style: 'short' })
// "a, b, & c"
anymany(['4 kg', '2 m'], { type: 'unit' })
// "4 kg, 2 m"
anymany(['cherry', 'apple', 'Banana'], { sort: true })
// "apple, Banana, and cherry"anymanyParts()
Same arguments as anymany(), but returns the output as { type, value } parts instead of a string — style the items apart from the separators, or rebuild the output your own way.
import { anymanyParts } from 'anymany'
anymanyParts(['a', 'b'])
// [
// { type: 'element', value: 'a' },
// { type: 'literal', value: ' and ' },
// { type: 'element', value: 'b' },
// ]
// React: bold the items
anymanyParts(tags).map((p, i) =>
p.type === 'element' ? <b key={i}>{p.value}</b> : p.value,
)Options
localestring | string[]default: runtime localeAny valid BCP 47 locale tag, or a fallback array — 'en', 'en-US', 'zh-TW', ['sr-Latn-RS', 'en'].
type'conjunction' | 'disjunction' | 'unit'default: 'conjunction'List flavor, mapped to Intl.ListFormat — 'a, b, and c' / 'a, b, or c' / 'a, b, c'.
style'long' | 'short' | 'narrow'default: 'long'Joiner wording length, mapped to Intl.ListFormat — 'and' / '&' / none.
sortboolean | 'numeric' | Intl.CollatorOptionsdefault: no sortingSort items with Intl.Collator before joining. true = default collation, 'numeric' = numeric collation, or any Intl.CollatorOptions for full control. Never mutates the input.
maxnumberdefault: no limitMaximum items to show (after sorting). The rest collapse into a trailing '+N' counter with localized digits. Throws RangeError when zero, negative, or fractional.
overflow(hidden: number) => stringdefault: `+${N}`Custom overflow label builder, replaces the default '+N'. Receives the number of hidden items.
Sorting
sort runs the items through Intl.Collator before joining — real language-aware collation, not code-point order. The input array is never mutated.
anymany(['cherry', 'apple', 'Banana'], { sort: true })
// "apple, Banana, and cherry" ← plain .sort() puts "Banana" first
anymany(['file10', 'file2'], { sort: 'numeric' })
// "file2 and file10" ← numbers compared by value
anymany(['a', 'A'], { sort: { caseFirst: 'upper' } })
// "A and a" ← any Intl.CollatorOptionsMax + overflow
max caps the visible items; the rest collapse into a trailing "+N" counter. Digits come from Intl.NumberFormat, so they localize — no words, locale-safe.
anymany(['x', 'y', 'z', 'a', 'b', 'c', 'd'], { max: 3 })
// "x, y, z, and +4"
anymany(['x', 'y', 'z', 'a', 'b'], { max: 3, overflow: (n) => `${n} more` })
// "x, y, z, and 2 more"The overflow item is just another list element, so the default type places an "and" before it. That is intentional — no hidden joiner magic. Prefer a plain comma list? Combine max with type: 'unit'.
Locales
Same calls in a few languages — no extra setup, no locale files.
anymany(['a', 'b', 'c'], { locale: 'en' }) // "a, b, and c"
anymany(['a', 'b', 'c'], { locale: 'ru' }) // "a, b и c"
anymany(['a', 'b', 'c'], { locale: 'de' }) // "a, b und c"
anymany(['a', 'b', 'c'], { locale: 'ja' }) // "a、b、c"
anymany(['a', 'b', 'c'], { type: 'disjunction', locale: 'ru' })
// "a, b или c"
anymany(['Öl', 'Zebra', 'Apfel'], { sort: true, locale: 'de' })
// "Apfel, Öl und Zebra" ← Ö sorts after A, not after Z
anymany(['файл10', 'файл2'], { sort: 'numeric', locale: 'ru' })
// "файл2 и файл10"
anymany(['a', 'b', 'c', 'd', 'e', 'f', 'g'], { max: 3, locale: 'ar-EG' })
// "a وb وc و+٤" ← localized overflow digitsPass any valid BCP 47 language tag — including regional variants like en-GB, zh-TW, or pt-BR. Locale is optional; when omitted, native Intl uses the runtime locale. Fallback arrays like ['sr-Latn-RS', 'en'] also work.
SSR
anymany is pure — same input, same output, no clocks, no randomness, no DOM. Server and client render identically as long as the locale is passed explicitly (the runtime locale may differ between server and browser).
import { anymany } from 'anymany'
export function TagList({ tags }: { tags: string[] }) {
return <p>{anymany(tags, { locale: 'en', sort: true, max: 5 })}</p>
}Compatibility
anymany uses Intl.ListFormat, Intl.Collator, and Intl.NumberFormat — all widely supported.
Limitations
A few things worth knowing before you ship:
Output depends on the runtime's Intl data
anymany delegates all formatting to native Intl. Exact output — joiner words, comma placement, the Oxford comma — may vary between Node versions, browsers, and regional variants (en vs en-GB). Don't hardcode expected strings in tests; use pattern matching instead.
No pluralization, by design
Intl ships no word data, and anymany ships zero language dictionaries — that is what keeps it lightweight and correct in every locale. The overflow counter is '+N' (localized digits) instead of 'and N more'. Need words? Pass your own via the overflow callback.
The overflow item is a regular list element
With max set, the '+N' counter goes through Intl.ListFormat like any other item, so conjunction mode reads 'x, y, z, and +4'. No hidden joiner magic — combine max with type: 'unit' for a plain comma list.
Node.js < 18
The package declares engines.node >= 18 and CI tests Node 20/22/24. Older versions down to 13 will usually work — the required Intl APIs are there — but they are unsupported and untested.