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 anymany

anymany()

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 locale

Any 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 sorting

Sort 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 limit

Maximum 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.CollatorOptions

Max + 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 digits

Pass 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.

Node.js18+CI runs the suite on Node 20, 22, 24
Chrome72+
Firefox78+
Safari14.1+
Edge79+
Vercel Edge Runtime
Cloudflare Workers
Deno

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.