Examples
Every working example of active-format on one page. Each one shows the file that renders it.
@workspace/active-format 1.0.0Developmentunreleased, in development in Foundationbaseline ce081dc3
All examples below are Client examples: they run entirely in your browser, with fixed input
values, so every reader sees the same input. The output is computed by your browser's Intl, so a
different browser version can write a value slightly differently.
One presentation for every formatterClient
| formatDateOnly | |
| formatDateTime | |
| formatNumber | |
| formatCurrency | |
| formatFractionAsPercent | |
| formatByteSize | |
| formatList |
// apps/docs/examples/format/presentation-switcher.tsx'use client';
import {useId, useState} from 'react';
import { composePresentationContext, formatByteSize, formatCurrency, formatDateOnly, formatDateTime, formatFractionAsPercent, formatList, formatNumber, toIanaTimeZone, type DateNotationPreset, type NumberNotationPreset, type PresentationProfileConfig, type TimeNotationPreset,} from '@workspace/active-format';import { Field, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import { NativeSelect, NativeSelectOption,} from '@workspace/active-ui/components/primitives/native-select';import { Table, TableBody, TableCell, TableRow,} from '@workspace/active-ui/components/primitives/table';
// Fixed values, so every reader sees the same input.const CALENDAR_DAY = '2026-03-09';const INSTANT = '2026-03-09T20:18:05Z';const LANGUAGES = ['en', 'nl', 'de', 'fr', 'es', 'ja', 'ar-EG'];const CURRENCIES = ['EUR', 'USD', 'GBP', 'JPY', 'KWD'];const TIME_ZONES = [ 'Europe/Amsterdam', 'America/New_York', 'Asia/Tokyo', 'UTC',];
type Choice<T> = T | 'language';
// What each notation choice shows. The value stays the preset name the// profile takes; the label says what it does, short enough to read in full// in a field on a narrow screen.const CHOICE_LABELS: Record<string, string> = { language: 'From the language', DMY_NUMERIC: 'Day, month, year', MDY_NUMERIC: 'Month, day, year', ISO_NUMERIC: 'Year, month, day (ISO)', H24: '24-hour clock', H12: '12-hour clock', DOT_DECIMAL_COMMA_GROUP: 'Decimal point, comma groups', COMMA_DECIMAL_DOT_GROUP: 'Decimal comma, point groups', COMMA_DECIMAL_SPACE_GROUP: 'Decimal comma, space groups',};
/** One presentation, composed from language, profile and zone, feeding every formatter. */export default function PresentationSwitcher() { const [language, setLanguage] = useState('en'); const [currency, setCurrency] = useState('EUR'); const [timeZone, setTimeZone] = useState('Europe/Amsterdam'); const [dateFormat, setDateFormat] = useState<Choice<DateNotationPreset>>('language'); const [timeFormat, setTimeFormat] = useState<Choice<TimeNotationPreset>>('language'); const [numberFormat, setNumberFormat] = useState<Choice<NumberNotationPreset>>('language');
// A profile field that is absent means "not configured": the language's own // convention applies. const profile: PresentationProfileConfig = { ...(dateFormat === 'language' ? {} : {dateFormat}), ...(timeFormat === 'language' ? {} : {timeFormat}), ...(numberFormat === 'language' ? {} : {numberFormat}), }; const presentation = composePresentationContext( language, profile, toIanaTimeZone(timeZone), );
const rows: [string, string][] = [ ['formatDateOnly', formatDateOnly(CALENDAR_DAY, 'long', presentation)], [ 'formatDateTime', formatDateTime(INSTANT, 'numeric-datetime', presentation), ], ['formatNumber', formatNumber(1234567.891, 'decimal-2', presentation)], [ 'formatCurrency', formatCurrency({amount: 1234.5, currency}, presentation), ], [ 'formatFractionAsPercent', formatFractionAsPercent(0.125, 'decimal-1', presentation), ], [ 'formatByteSize', formatByteSize(1_536_000, presentation, {system: 'decimal'}), ], ['formatList', formatList(['Amsterdam', 'Utrecht', 'Delft'], presentation)], ];
// The stage centres its content, which sizes a plain root to its widest // child: `w-full min-w-0` keeps the demo to the width there is, so the // fields wrap and only the result table scrolls, in its own named region. return ( <div className="flex w-full min-w-0 flex-col gap-4"> {/* Columns follow the width the demo has, not the screen: beside the sidebar a three-column row at `sm` left each field too narrow for its longest choice. */} <div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,15rem),1fr))] gap-3"> <Select label="Language" value={language} onChange={setLanguage} options={LANGUAGES} /> <Select label="Currency" value={currency} onChange={setCurrency} options={CURRENCIES} /> <Select label="Time zone" value={timeZone} onChange={setTimeZone} options={TIME_ZONES} /> <Select label="Date notation" value={dateFormat} onChange={(value) => setDateFormat(value as Choice<DateNotationPreset>) } options={['language', 'DMY_NUMERIC', 'MDY_NUMERIC', 'ISO_NUMERIC']} /> <Select label="Clock" value={timeFormat} onChange={(value) => setTimeFormat(value as Choice<TimeNotationPreset>) } options={['language', 'H24', 'H12']} /> <Select label="Number notation" value={numberFormat} onChange={(value) => setNumberFormat(value as Choice<NumberNotationPreset>) } options={[ 'language', 'DOT_DECIMAL_COMMA_GROUP', 'COMMA_DECIMAL_DOT_GROUP', 'COMMA_DECIMAL_SPACE_GROUP', ]} /> </div> <Table scrollLabel="Results, scrolls sideways"> <TableBody> {rows.map(([name, text]) => ( <TableRow key={name}> <TableCell className="text-muted-foreground font-mono text-xs"> {name} </TableCell> <TableCell> <output suppressHydrationWarning>{text}</output> </TableCell> </TableRow> ))} </TableBody> </Table> </div> );}
function Select({ label, value, onChange, options,}: { label: string; value: string; onChange: (value: string) => void; options: readonly string[];}) { const id = useId(); return ( <Field className="min-w-0"> <FieldLabel htmlFor={id}>{label}</FieldLabel> <NativeSelect id={id} className="w-full" value={value} onChange={(event) => onChange(event.target.value)} > {options.map((option) => ( <NativeSelectOption key={option} value={option}> {CHOICE_LABELS[option] ?? option} </NativeSelectOption> ))} </NativeSelect> </Field> );}Currency amountsClient
| EUR, en | |
| EUR, nl | |
| JPY (no minor unit) | |
| KWD (three decimals) | |
| Whole amounts | |
| A change, signed | |
| Exact decimal string | |
| The same value as a number | |
| No amount |
// apps/docs/examples/format/currency-amounts.tsx'use client';
import {formatCurrency, toDecimalString} from '@workspace/active-format';import { Table, TableBody, TableCell, TableRow,} from '@workspace/active-ui/components/primitives/table';
const EXACT = '12345678901234567.89';
/** * The currency comes from the data, the notation from the presentation. * Decimals follow the currency's minor units unless the call overrides them. */export default function CurrencyAmounts() { const rows: [string, string][] = [ ['EUR, en', formatCurrency({amount: 1234.5, currency: 'EUR'}, 'en')], ['EUR, nl', formatCurrency({amount: 1234.5, currency: 'EUR'}, 'nl')], [ 'JPY (no minor unit)', formatCurrency({amount: 1234.5, currency: 'JPY'}, 'en'), ], [ 'KWD (three decimals)', formatCurrency({amount: 1234.5, currency: 'KWD'}, 'en'), ], [ 'Whole amounts', formatCurrency({amount: 1234.5, currency: 'EUR'}, 'en', {decimals: 0}), ], [ 'A change, signed', formatCurrency({amount: 12.5, currency: 'EUR'}, 'en', { sign: 'exceptZero', }), ], [ 'Exact decimal string', formatCurrency({amount: toDecimalString(EXACT), currency: 'EUR'}, 'en'), ], [ 'The same value as a number', formatCurrency({amount: Number(EXACT), currency: 'EUR'}, 'en'), ], [ 'No amount', formatCurrency({amount: null, currency: 'EUR'}, 'en') || '(empty string)', ], ]; return ( <Table> <TableBody> {rows.map(([label, text]) => ( <TableRow key={label}> <TableCell className="text-muted-foreground">{label}</TableCell> <TableCell className="font-mono"> <output suppressHydrationWarning>{text}</output> </TableCell> </TableRow> ))} </TableBody> </Table> );}Calendar day versus instantClient
| Zone | The instant | Its day there | The calendar day |
|---|---|---|---|
| UTC | 9 Mar 2026, 23:30 | 2026-03-09 | 9 March 2026 |
| Europe/Amsterdam | 10 Mar 2026, 00:30 | 2026-03-10 | 9 March 2026 |
| America/New_York | 9 Mar 2026, 19:30 | 2026-03-09 | 9 March 2026 |
| Asia/Tokyo | 10 Mar 2026, 08:30 | 2026-03-10 | 9 March 2026 |
// apps/docs/examples/format/calendar-day-vs-instant.tsx'use client';
import { calendarDayOf, composePresentationContext, formatDateOnly, formatDateTime, toIanaTimeZone,} from '@workspace/active-format';import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@workspace/active-ui/components/primitives/table';
// One moment, late in the evening in UTC.const INSTANT = '2026-03-09T23:30:00Z';// One calendar day: a birthday, a due date. It has no zone.const CALENDAR_DAY = '2026-03-09';const ZONES = ['UTC', 'Europe/Amsterdam', 'America/New_York', 'Asia/Tokyo'];
/** * An instant reads differently per zone; a calendar day never moves. */export default function CalendarDayVsInstant() { return ( <Table> <TableHeader> <TableRow> <TableHead>Zone</TableHead> <TableHead>The instant</TableHead> <TableHead>Its day there</TableHead> <TableHead>The calendar day</TableHead> </TableRow> </TableHeader> <TableBody> {ZONES.map((zone) => { const timeZone = toIanaTimeZone(zone); const presentation = composePresentationContext('en', null, timeZone); return ( <TableRow key={zone}> <TableCell className="font-mono text-xs">{zone}</TableCell> <TableCell suppressHydrationWarning> {formatDateTime(INSTANT, 'datetime', presentation)} </TableCell> <TableCell className="font-mono text-xs"> {calendarDayOf(INSTANT, timeZone)} </TableCell> <TableCell suppressHydrationWarning> {formatDateOnly(CALENDAR_DAY, 'long', presentation)} </TableCell> </TableRow> ); })} </TableBody> </Table> );}Relative timeClient
| Value | mode | text |
|---|---|---|
| 2026-03-09T11:58:00Z | relative | 2 minutes ago |
| 2026-03-08T09:00:00Z | relative | 1 day ago |
| 2026-03-12T12:00:00Z | relative | in 3 days |
| 2026-02-01T08:00:00Z | absolute | 1 Feb 2026, 08:00 |
| 2026-03-09 | unavailable | (none) |
// apps/docs/examples/format/recency.tsx'use client';
import {resolveRecency} from '@workspace/active-format';import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@workspace/active-ui/components/primitives/table';
// A fixed "now": the example reads no clock, so every reader sees the same.const NOW = new Date('2026-03-09T12:00:00Z');const VALUES = [ '2026-03-09T11:58:00Z', '2026-03-08T09:00:00Z', '2026-03-12T12:00:00Z', '2026-02-01T08:00:00Z', '2026-03-09',];
/** * Relative within the threshold (seven days by default), absolute beyond it, * unavailable for a value that is not an instant. */export default function Recency() { return ( <Table> <TableHeader> <TableRow> <TableHead>Value</TableHead> <TableHead>mode</TableHead> <TableHead>text</TableHead> </TableRow> </TableHeader> <TableBody> {VALUES.map((value) => { const result = resolveRecency(value, 'en', {now: NOW}); return ( <TableRow key={value}> <TableCell className="font-mono text-xs">{value}</TableCell> <TableCell className="font-mono text-xs">{result.mode}</TableCell> <TableCell suppressHydrationWarning> {result.text || '(none)'} </TableCell> </TableRow> ); })} </TableBody> </Table> );}Reading a typed numberClient
- status
- valid
- value
- 1234.5
- number
- 1234.5
- rejection
- (none)
// apps/docs/examples/format/number-input.tsx'use client';
import {useId, useState} from 'react';
import { classifyNumberInput, getNumberInputPlaceholder,} from '@workspace/active-format';import { DescriptionItem, DescriptionList,} from '@workspace/active-ui/components/composed/description-list';import { Field, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';import { NativeSelect, NativeSelectOption,} from '@workspace/active-ui/components/primitives/native-select';
/** * Reading a typed number back with the same grammar that writes it. The * result says whether the text is empty, incomplete, invalid or valid, and * why a text is refused. Grouping separators are refused while typing and * accepted in pasted text. */export default function NumberInput() { const inputId = useId(); const languageId = useId(); const modeId = useId(); const [language, setLanguage] = useState('nl'); const [mode, setMode] = useState<'typing' | 'paste'>('typing'); const [text, setText] = useState('1234,5'); const result = classifyNumberInput(text, language, {mode, maxDecimals: 2}); return ( <div className="flex flex-col gap-4"> <div className="grid gap-3 sm:grid-cols-3"> <Field> <FieldLabel htmlFor={languageId}>Language</FieldLabel> <NativeSelect id={languageId} value={language} onChange={(event) => setLanguage(event.target.value)} > {['nl', 'en', 'de', 'fr'].map((option) => ( <NativeSelectOption key={option} value={option}> {option} </NativeSelectOption> ))} </NativeSelect> </Field> <Field> <FieldLabel htmlFor={modeId}>Input</FieldLabel> <NativeSelect id={modeId} value={mode} onChange={(event) => setMode(event.target.value === 'paste' ? 'paste' : 'typing') } > <NativeSelectOption value="typing">Typed</NativeSelectOption> <NativeSelectOption value="paste">Pasted</NativeSelectOption> </NativeSelect> </Field> <Field> <FieldLabel htmlFor={inputId}> Amount (at most two decimals) </FieldLabel> <Input id={inputId} value={text} placeholder={getNumberInputPlaceholder(language, 2)} onChange={(event) => setText(event.target.value)} inputMode="decimal" /> </Field> </div> <DescriptionList orientation="inline" columns={1}> <DescriptionItem label="status"> <span className="font-mono" data-testid="number-status"> {result.status} </span> </DescriptionItem> <DescriptionItem label="value"> <span className="font-mono">{result.value || '(none)'}</span> </DescriptionItem> <DescriptionItem label="number"> <span className="font-mono">{result.number ?? 'null'}</span> </DescriptionItem> <DescriptionItem label="rejection"> <span className="font-mono">{result.rejection ?? '(none)'}</span> </DescriptionItem> </DescriptionList> </div> );}Reading a typed dateClient
- status
- valid
- value
- 2028-02-29
- read as
- Dienstag, 29. Februar 2028
// apps/docs/examples/format/date-input.tsx'use client';
import {useId, useState} from 'react';
import { classifyDateInput, formatDateOnly, getDateInputPattern,} from '@workspace/active-format';import { DescriptionItem, DescriptionList,} from '@workspace/active-ui/components/composed/description-list';import { Field, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';import { NativeSelect, NativeSelectOption,} from '@workspace/active-ui/components/primitives/native-select';
/** * A typed date in the reader's notation, read back as an ISO calendar day. * Impossible dates are invalid, never moved to the next valid day. */export default function DateInput() { const inputId = useId(); const languageId = useId(); const [language, setLanguage] = useState('de'); const [text, setText] = useState('29.02.2028'); const pattern = getDateInputPattern(language); const result = classifyDateInput(text, language); return ( <div className="flex flex-col gap-4"> <div className="grid gap-3 sm:grid-cols-2"> <Field> <FieldLabel htmlFor={languageId}>Language</FieldLabel> <NativeSelect id={languageId} value={language} onChange={(event) => setLanguage(event.target.value)} > {['de', 'nl', 'en', 'fr'].map((option) => ( <NativeSelectOption key={option} value={option}> {option} </NativeSelectOption> ))} </NativeSelect> </Field> <Field> <FieldLabel htmlFor={inputId}>Date ({pattern})</FieldLabel> <Input id={inputId} value={text} placeholder={pattern} onChange={(event) => setText(event.target.value)} /> </Field> </div> <DescriptionList orientation="inline" columns={1}> <DescriptionItem label="status"> <span className="font-mono" data-testid="date-status"> {result.status} </span> </DescriptionItem> <DescriptionItem label="value"> <span className="font-mono">{result.value || '(none)'}</span> </DescriptionItem> <DescriptionItem label="read as"> <span suppressHydrationWarning> {result.status === 'valid' ? formatDateOnly(result.value, 'full', language) : '(none)'} </span> </DescriptionItem> </DescriptionList> </div> );}Unsupported language tagsClient
| Tag | isSupportedLocale | formatNumber(1234.5) |
|---|---|---|
| en-GB | true | 1,234.5 |
| nl | true | 1.234,5 |
| pt-BR | true | 1.234,5 |
| zz-ZZ | false | UnsupportedLocaleError (unsupported) |
| en_US | false | UnsupportedLocaleError (malformed) |
| ja-JP-u-ca-japanese | false | UnsupportedLocaleError (calendar) |
// apps/docs/examples/format/locale-errors.tsx'use client';
import { formatNumber, isSupportedLocale, UnsupportedLocaleError,} from '@workspace/active-format';import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@workspace/active-ui/components/primitives/table';
// Tags a host might receive from a profile, a header or a setting.const TAGS = ['en-GB', 'nl', 'pt-BR', 'zz-ZZ', 'en_US', 'ja-JP-u-ca-japanese'];
function tryFormat(tag: string): string { try { return formatNumber(1234.5, 'decimal-1', tag); } catch (err) { if (err instanceof UnsupportedLocaleError) { return `UnsupportedLocaleError (${err.reason})`; } throw err; }}
/** * A refused tag throws: there is no silent English or host default. A host * that wants to test a tag first asks `isSupportedLocale`. */export default function LocaleErrors() { return ( <Table> <TableHeader> <TableRow> <TableHead>Tag</TableHead> <TableHead>isSupportedLocale</TableHead> <TableHead>formatNumber(1234.5)</TableHead> </TableRow> </TableHeader> <TableBody> {TAGS.map((tag) => ( <TableRow key={tag}> <TableCell className="font-mono text-xs">{tag}</TableCell> <TableCell className="font-mono text-xs" suppressHydrationWarning> {String(isSupportedLocale(tag))} </TableCell> <TableCell suppressHydrationWarning>{tryFormat(tag)}</TableCell> </TableRow> ))} </TableBody> </Table> );}