ComponentsMisc
Format
Writes numbers, amounts of money, file sizes, dates and relative times in the reader's locale and time zone.
Import
import { FormatNumber, FormatCurrency, FormatBytes, FormatDate, FormatRelativeTime } from "@booleanpress/ui/format"Usage
Each part writes one value with Intl, in the locale and time zone set on BooleanUIProvider, inside a data or time element that keeps the raw value for machines.
import { FormatBytes, FormatDate, FormatNumber, FormatRelativeTime } from "@booleanpress/ui/format"
export function DeliveryRow({ log }: { log: DeliveryLog }) {
return (
<p>
<FormatNumber value={log.recipients} /> recipients, <FormatBytes value={log.size} />, sent{" "}
<FormatRelativeTime value={log.sentAt} live /> (<FormatDate value={log.sentAt} dateStyle="medium" timeStyle="short" />)
</p>
)
}FormatNumbertakes astyle:decimal(the default),percent(the value is a fraction: 0.42 is 42%),compact(48,250 is 48K) orunitwith aunitsuch as"millisecond". Any otherIntl.NumberFormatoption is a prop:maximumFractionDigits,signDisplay,notation.FormatCurrencytakes an ISO 4217currencycode and the same options:currencySign="accounting"writes a refund in brackets,notation="compact"writes $483K.FormatBytessteps by 1,000 and writes the short symbols B, KB, MB and GB, the same in every language, after the number in the locale's digits, separators and spacing ("1,5 MB" in German): the wayFileUploadandChatwrite sizes, as they share one formatter.unitDisplay(short,narrow,long) writes the locale's own unit throughIntlinstead ("512 bytes" long; ko, Mo and Go in French).units="binary"steps by 1,024 with the IEC symbols KiB, MiB and GiB: use it where a size is a power of two, such as a memory or mailbox quota.FormatDatetakesdateStyleandtimeStyle, or anyIntl.DateTimeFormatfields; with neither it writes a medium date ("Oct 14, 2026"). The time zone is the provider'stimeZone, or thetimeZoneprop. A date-only string ("2026-10-31") is a calendar day and reads as that day in every time zone.FormatRelativeTimewrites the time fromnowto the value ("3 hours ago", "in 3 days", "yesterday"), in the largest unit that keeps the number at 1 or more.livewrites it again every minute while it is shown; passnowfor a fixed reference, as in a test or a screenshot. Withoutnow, a page rendered on a server writes it again against the browser's clock once it is hydrated, so a cached page never shows the server's old "now".
Every part takes locale to write one value in another language. Each one has a plain function beside it for code outside JSX, such as a chart's axis or a CSV export: formatNumber, formatCurrency, formatBytes, formatDate and formatRelativeTime. They take the value and an options object with locale (and timeZone for dates), and do not read the provider: inside a component, pass them useUiLocale()'s values.
import { formatBytes } from "@booleanpress/ui/format"
formatBytes(1_572_864, { locale: "de-DE", units: "binary" }) // "1,5 MiB"Set the provider's locale and timeZone when the page is rendered on a server, so the server and the browser write the same text.
Examples
Numbers
FormatNumber as a plain number, a percentage, a compact count, a unit and a signed change.
import { FormatNumber } from "@booleanpress/ui/format"
export default function FormatNumbers() {
return (
<dl className="grid w-full max-w-sm grid-cols-[1fr_auto] gap-x-6 gap-y-2 text-sm/normal">
<dt className="text-muted-foreground">Emails sent</dt>
<dd className="text-end tabular-nums">
<FormatNumber value={1240861} />
</dd>
<dt className="text-muted-foreground">Open rate</dt>
<dd className="text-end tabular-nums">
<FormatNumber value={0.4286} style="percent" maximumFractionDigits={1} />
</dd>
<dt className="text-muted-foreground">Subscribers</dt>
<dd className="text-end tabular-nums">
<FormatNumber value={48250} style="compact" />
</dd>
<dt className="text-muted-foreground">Average send time</dt>
<dd className="text-end tabular-nums">
<FormatNumber value={245} style="unit" unit="millisecond" />
</dd>
<dt className="text-muted-foreground">Change since last week</dt>
<dd className="text-end tabular-nums">
<FormatNumber value={0.031} style="percent" signDisplay="exceptZero" maximumFractionDigits={1} />
</dd>
</dl>
)
}Currency
FormatCurrency in three currencies, with an accounting refund, a compact total and a third decimal.
import { FormatCurrency } from "@booleanpress/ui/format"
export default function FormatCurrencyExample() {
return (
<dl className="grid w-full max-w-sm grid-cols-[1fr_auto] gap-x-6 gap-y-2 text-sm/normal">
<dt className="text-muted-foreground">Business plan, monthly</dt>
<dd className="text-end tabular-nums">
<FormatCurrency value={29} currency="USD" />
</dd>
<dt className="text-muted-foreground">Invoice INV-2026-0142</dt>
<dd className="text-end tabular-nums">
<FormatCurrency value={1234.5} currency="EUR" />
</dd>
<dt className="text-muted-foreground">Refund to Northwind Ltd</dt>
<dd className="text-end tabular-nums">
<FormatCurrency value={-45} currency="GBP" currencySign="accounting" />
</dd>
<dt className="text-muted-foreground">Revenue this year</dt>
<dd className="text-end tabular-nums">
<FormatCurrency value={482500} currency="USD" notation="compact" />
</dd>
<dt className="text-muted-foreground">Price per 1,000 emails</dt>
<dd className="text-end tabular-nums">
<FormatCurrency value={0.085} currency="USD" maximumFractionDigits={3} />
</dd>
</dl>
)
}Bytes
FormatBytes in steps of 1,000 (B, KB, MB, GB) and, with units="binary", of 1,024 (KiB, MiB, GiB).
import { FormatBytes } from "@booleanpress/ui/format"
const files = [
{ name: "unsubscribe.txt", size: 512 },
{ name: "invoice-0142.pdf", size: 245_760 },
{ name: "delivery-log-october.csv", size: 3_670_016 },
{ name: "mailbox-export.zip", size: 2_147_483_648 },
]
export default function FormatBytesExample() {
return (
<table className="w-full max-w-md text-sm/normal">
<thead>
<tr className="border-b text-muted-foreground">
<th className="py-2 text-start font-medium">File</th>
<th className="py-2 text-end font-medium">Decimal</th>
<th className="py-2 text-end font-medium">Binary</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr key={file.name} className="border-b last:border-0">
<td className="py-2">{file.name}</td>
<td className="py-2 text-end tabular-nums">
<FormatBytes value={file.size} />
</td>
<td className="py-2 text-end tabular-nums">
<FormatBytes value={file.size} units="binary" />
</td>
</tr>
))}
</tbody>
</table>
)
}Dates and times
FormatDate with dateStyle, timeStyle and fields of your own, and a calendar day given as a date-only string.
import { FormatDate } from "@booleanpress/ui/format"
// An instant from the API, and the time zone of the site it belongs to.
const SCHEDULED = "2026-10-14T09:30:00Z"
const ZONE = "Europe/London"
export default function FormatDatesAndTimes() {
return (
<dl className="grid w-full max-w-md grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-sm/normal">
<dt className="text-muted-foreground">Medium date</dt>
<dd>
<FormatDate value={SCHEDULED} timeZone={ZONE} />
</dd>
<dt className="text-muted-foreground">Full date</dt>
<dd>
<FormatDate value={SCHEDULED} dateStyle="full" timeZone={ZONE} />
</dd>
<dt className="text-muted-foreground">Date and time</dt>
<dd>
<FormatDate value={SCHEDULED} dateStyle="medium" timeStyle="short" timeZone={ZONE} />
</dd>
<dt className="text-muted-foreground">Time only</dt>
<dd>
<FormatDate value={SCHEDULED} timeStyle="short" timeZone={ZONE} />
</dd>
<dt className="text-muted-foreground">Your own fields</dt>
<dd>
<FormatDate value={SCHEDULED} weekday="short" day="numeric" month="short" hour="numeric" minute="2-digit" timeZone={ZONE} />
</dd>
<dt className="text-muted-foreground">A calendar day</dt>
<dd>
<FormatDate value="2026-10-31" dateStyle="long" />
</dd>
</dl>
)
}Relative time
FormatRelativeTime against a fixed now: seconds, minutes, hours, yesterday, weeks and a future date.
import { FormatRelativeTime } from "@booleanpress/ui/format"
// A fixed "now", so the example reads the same every day; leave `now` out (and add `live`) in an app.
const NOW = "2026-10-14T12:00:00Z"
const events = [
{ id: "evt_9f2", text: "Delivered to anna@example.com", at: "2026-10-14T11:59:15Z" },
{ id: "evt_9f1", text: "Opened by mark@example.com", at: "2026-10-14T11:48:00Z" },
{ id: "evt_9e7", text: "Bounced: mailbox full", at: "2026-10-14T09:02:00Z" },
{ id: "evt_9c4", text: "API key rotated", at: "2026-10-13T08:30:00Z" },
{ id: "evt_9a0", text: "Domain verified", at: "2026-09-29T10:00:00Z" },
{ id: "evt_a01", text: "Newsletter scheduled", at: "2026-10-17T09:00:00Z" },
]
export default function FormatRelativeTimeExample() {
return (
<ul className="w-full max-w-md divide-y text-sm/normal">
{events.map((event) => (
<li key={event.id} className="flex items-center justify-between gap-4 py-2">
<span>{event.text}</span>
<FormatRelativeTime value={event.at} now={NOW} className="shrink-0 text-muted-foreground" />
</li>
))}
<li className="flex items-center justify-between gap-4 py-2">
<span>Short style, always a number</span>
<FormatRelativeTime value="2026-10-13T12:00:00Z" now={NOW} numeric="always" style="short" className="shrink-0 text-muted-foreground" />
</li>
</ul>
)
}Another locale
The same figures under a provider set to de-DE and one set to ar-EG, with that locale's digits and separators.
import { BooleanUIProvider } from "@booleanpress/ui/provider"
import { FormatBytes, FormatCurrency, FormatDate, FormatNumber, FormatRelativeTime } from "@booleanpress/ui/format"
const NOW = "2026-10-14T12:00:00Z"
function Figures({ currency }: { currency: string }) {
return (
<ul className="space-y-1 text-sm/normal tabular-nums">
<li>
<FormatNumber value={1240861.5} />
</li>
<li>
<FormatCurrency value={1234.5} currency={currency} />
</li>
<li>
<FormatNumber value={0.4286} style="percent" maximumFractionDigits={1} />
</li>
<li>
<FormatBytes value={3_670_016} />
</li>
<li>
<FormatDate value="2026-10-14" dateStyle="long" />
</li>
<li>
<FormatRelativeTime value="2026-10-14T09:00:00Z" now={NOW} />
</li>
</ul>
)
}
export default function FormatAnotherLocale() {
return (
<div className="grid w-full max-w-md grid-cols-2 gap-6">
<figure lang="de-DE" className="space-y-2">
<figcaption className="font-medium">Deutsch</figcaption>
<BooleanUIProvider locale="de-DE">
<Figures currency="EUR" />
</BooleanUIProvider>
</figure>
<figure lang="ar-EG" dir="rtl" className="space-y-2">
<figcaption className="font-medium">العربية</figcaption>
<BooleanUIProvider locale="ar-EG">
<Figures currency="EGP" />
</BooleanUIProvider>
</figure>
</div>
)
}Time zone
One instant under providers set to three time zones, and the timeZone prop writing it in UTC.
import { BooleanUIProvider } from "@booleanpress/ui/provider"
import { FormatDate } from "@booleanpress/ui/format"
// One send, at one instant, shown where each team works.
const SENT = "2026-10-14T09:30:00Z"
const offices = [
{ city: "Berlin", timeZone: "Europe/Berlin" },
{ city: "New York", timeZone: "America/New_York" },
{ city: "Tokyo", timeZone: "Asia/Tokyo" },
]
export default function FormatTimeZone() {
return (
<dl className="grid w-full max-w-md grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-sm/normal">
{offices.map((office) => (
<BooleanUIProvider key={office.city} locale="en-US" timeZone={office.timeZone}>
<dt className="text-muted-foreground">{office.city}</dt>
<dd>
<FormatDate value={SENT} dateStyle="medium" timeStyle="short" />
</dd>
</BooleanUIProvider>
))}
<dt className="text-muted-foreground">UTC, by prop</dt>
<dd>
<FormatDate value={SENT} dateStyle="medium" timeStyle="long" timeZone="UTC" />
</dd>
</dl>
)
}Accessibility
- Semantics
- A number, amount or size is a
dataelement whosevalueholds the raw number; a date or relative time is atimeelement whosedateTimeholds the ISO 8601 instant (or the day, for a date-only value). Both are read as the text inside them. - Labels
- A formatted value has no name of its own: put it next to words that say what it is, as a table cell under its heading or a description list's
ddafter itsdt. Wrap a value in another language in an element with thatlang, so a screen reader pronounces it in that language. - Focus
- The parts take no focus.
- Known limits
- A live relative time changes its text every minute without announcing it; that is on purpose, as a ticking announcement would interrupt. Wrap it in your own
aria-liveregion if a change must be heard. - Unit symbols and short styles ("KB", "3 hr. ago") are read as screen readers expand them, which varies, and a bare "B" is easy to miss. Use
unitDisplay="long"orstyle="long"where the full word matters. - The entry is a client module (it reads the provider), so a React Server Component cannot call the plain functions from it; call
Intlthere instead. - An invalid date or a NaN writes nothing, and the element keeps no
valueordateTime.
- A live relative time changes its text every minute without announcing it; that is on purpose, as a ticking announcement would interrupt. Wrap it in your own
Keyboard
| Key | Behaviour |
|---|
API
FormatNumber
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | FormatNumberValue | The number to write. | |
compactDisplay | "short" | "long" | ||
currency | string | ||
currencyDisplay | "symbol" | "code" | "name" | "narrowSymbol" | ||
currencySign | "standard" | "accounting" | ||
locale | string | BCP 47 locale ("de-DE"); undefined is the runtime's default. | |
localeMatcher | "lookup" | "best fit" | ||
maximumFractionDigits | number | ||
maximumSignificantDigits | number | ||
minimumFractionDigits | number | ||
minimumIntegerDigits | number | ||
minimumSignificantDigits | number | ||
notation | "compact" | "standard" | "scientific" | "engineering" | ||
numberingSystem | string | ||
signDisplay | "auto" | "always" | "never" | "exceptZero" | ||
style | "decimal" | "unit" | "percent" | "compact" | decimal (the default), percent, compact or unit (with a unit, such as "millisecond"). | |
unit | string | ||
unitDisplay | "short" | "long" | "narrow" | ||
useGrouping | boolean |
FormatCurrency
| Prop | Type | Default | Description |
|---|---|---|---|
currencyrequired | string | The ISO 4217 code of the currency: "USD", "EUR". | |
valuerequired | FormatNumberValue | The amount to write, in the currency's main unit (dollars, not cents). | |
compactDisplay | "short" | "long" | ||
currencyDisplay | "symbol" | "code" | "name" | "narrowSymbol" | ||
currencySign | "standard" | "accounting" | ||
locale | string | BCP 47 locale ("de-DE"); undefined is the runtime's default. | |
localeMatcher | "lookup" | "best fit" | ||
maximumFractionDigits | number | ||
maximumSignificantDigits | number | ||
minimumFractionDigits | number | ||
minimumIntegerDigits | number | ||
minimumSignificantDigits | number | ||
notation | "compact" | "standard" | "scientific" | "engineering" | ||
numberingSystem | string | ||
signDisplay | "auto" | "always" | "never" | "exceptZero" | ||
unit | string | ||
unitDisplay | "short" | "long" | "narrow" | ||
useGrouping | boolean |
FormatBytes
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | number | The size in bytes. | |
locale | string | BCP 47 locale ("de-DE"); undefined is the runtime's default. | |
maximumFractionDigits | number | The most decimals shown for kilobytes and above (bytes are whole). Defaults to 1. | |
unitDisplay | "short" | "long" | "narrow" | How the unit is written. By default it is the short symbol, the same in every language ("512 B", "1.5 MB"), as
FileUpload and Chat write sizes. short, narrow and long use the locale's own unit through Intl ("512 byte",
"1,5 Mo" in French, "512 bytes" long); the binary steps keep their IEC symbols. | |
units | "decimal" | "binary" | decimal (the default): steps of 1,000, with the symbols B, KB, MB, GB. binary: steps of 1,024, with the IEC
symbols KiB, MiB, GiB. |
FormatDate
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | FormatDateValue | The date: a Date, a timestamp, or an ISO 8601 string ("2026-10-14T09:30:00Z", or "2026-10-14" for a day). | |
calendar | string | ||
dateStyle | "full" | "short" | "long" | "medium" | ||
day | "numeric" | "2-digit" | ||
dayPeriod | "short" | "long" | "narrow" | ||
era | "short" | "long" | "narrow" | ||
formatMatcher | "best fit" | "basic" | ||
fractionalSecondDigits | 1 | 2 | 3 | ||
hour | "numeric" | "2-digit" | ||
hour12 | boolean | ||
hourCycle | "h11" | "h12" | "h23" | "h24" | ||
locale | string | BCP 47 locale ("de-DE"); undefined is the runtime's default. | |
localeMatcher | "lookup" | "best fit" | ||
minute | "numeric" | "2-digit" | ||
month | "numeric" | "short" | "long" | "narrow" | "2-digit" | ||
numberingSystem | string | ||
second | "numeric" | "2-digit" | ||
timeStyle | "full" | "short" | "long" | "medium" | ||
timeZone | string | The IANA time zone to write the date in ("Europe/Berlin"). Defaults to the provider's timeZone, else the browser's. | |
timeZoneName | "short" | "long" | "shortOffset" | "longOffset" | "shortGeneric" | "longGeneric" | ||
weekday | "short" | "long" | "narrow" | ||
year | "numeric" | "2-digit" |
FormatRelativeTime
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | FormatDateValue | The date: a Date, a timestamp, or an ISO 8601 string. | |
live | boolean | false | Writes the time again every minute, against the current time, while it is shown. Ignored when now is given. |
locale | string | BCP 47 locale ("de-DE"); undefined is the runtime's default. | |
now | FormatDateValue | The moment the value is compared with. Defaults to the current time. | |
numeric | "auto" | "always" | auto (the default) writes "yesterday" and "now"; always writes "1 day ago" and "in 0 seconds". | |
style | "short" | "long" | "narrow" | long (the default, "3 hours ago"), short ("3 hr. ago") or narrow ("3h ago"). | |
unit | "month" | "week" | "day" | "year" | "second" | "minute" | "hour" | The unit to write the difference in. By default the largest unit that keeps the number at 1 or more. |
Also exported: formatNumber, a helper the parts use; formatCurrency, a helper the parts use; formatBytes, a helper the parts use; formatDate, a helper the parts use; formatRelativeTime, a helper the parts use.
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="format-number" (FormatNumber), data-slot="format-currency" (FormatCurrency), data-slot="format-bytes" (FormatBytes), data-slot="format-date" (FormatDate), data-slot="format-relative-time" (FormatRelativeTime), and data-currency.
Theming
The parts draw nothing of their own: they take the font and colour of the text around them. Add tabular-nums (font-variant-numeric: tabular-nums) where figures line up in a column.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|