Skip to the content

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>
  )
}
  • FormatNumber takes a style: decimal (the default), percent (the value is a fraction: 0.42 is 42%), compact (48,250 is 48K) or unit with a unit such as "millisecond". Any other Intl.NumberFormat option is a prop: maximumFractionDigits, signDisplay, notation.
  • FormatCurrency takes an ISO 4217 currency code and the same options: currencySign="accounting" writes a refund in brackets, notation="compact" writes $483K.
  • FormatBytes steps 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 way FileUpload and Chat write sizes, as they share one formatter. unitDisplay (short, narrow, long) writes the locale's own unit through Intl instead ("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.
  • FormatDate takes dateStyle and timeStyle, or any Intl.DateTimeFormat fields; with neither it writes a medium date ("Oct 14, 2026"). The time zone is the provider's timeZone, or the timeZone prop. A date-only string ("2026-10-31") is a calendar day and reads as that day in every time zone.
  • FormatRelativeTime writes the time from now to the value ("3 hours ago", "in 3 days", "yesterday"), in the largest unit that keeps the number at 1 or more. live writes it again every minute while it is shown; pass now for a fixed reference, as in a test or a screenshot. Without now, 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 data element whose value holds the raw number; a date or relative time is a time element whose dateTime holds 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 dd after its dt. Wrap a value in another language in an element with that lang, 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-live region 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" or style="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 Intl there instead.
  • An invalid date or a NaN writes nothing, and the element keeps no value or dateTime.

Keyboard

Keyboard
KeyBehaviour

API

FormatNumber

FormatNumber props
PropTypeDefaultDescription
valuerequiredFormatNumberValueThe number to write.
compactDisplay"short" | "long"
currencystring
currencyDisplay"symbol" | "code" | "name" | "narrowSymbol"
currencySign"standard" | "accounting"
localestringBCP 47 locale ("de-DE"); undefined is the runtime's default.
localeMatcher"lookup" | "best fit"
maximumFractionDigitsnumber
maximumSignificantDigitsnumber
minimumFractionDigitsnumber
minimumIntegerDigitsnumber
minimumSignificantDigitsnumber
notation"compact" | "standard" | "scientific" | "engineering"
numberingSystemstring
signDisplay"auto" | "always" | "never" | "exceptZero"
style"decimal" | "unit" | "percent" | "compact"decimal (the default), percent, compact or unit (with a unit, such as "millisecond").
unitstring
unitDisplay"short" | "long" | "narrow"
useGroupingboolean

FormatCurrency

FormatCurrency props
PropTypeDefaultDescription
currencyrequiredstringThe ISO 4217 code of the currency: "USD", "EUR".
valuerequiredFormatNumberValueThe amount to write, in the currency's main unit (dollars, not cents).
compactDisplay"short" | "long"
currencyDisplay"symbol" | "code" | "name" | "narrowSymbol"
currencySign"standard" | "accounting"
localestringBCP 47 locale ("de-DE"); undefined is the runtime's default.
localeMatcher"lookup" | "best fit"
maximumFractionDigitsnumber
maximumSignificantDigitsnumber
minimumFractionDigitsnumber
minimumIntegerDigitsnumber
minimumSignificantDigitsnumber
notation"compact" | "standard" | "scientific" | "engineering"
numberingSystemstring
signDisplay"auto" | "always" | "never" | "exceptZero"
unitstring
unitDisplay"short" | "long" | "narrow"
useGroupingboolean

FormatBytes

FormatBytes props
PropTypeDefaultDescription
valuerequirednumberThe size in bytes.
localestringBCP 47 locale ("de-DE"); undefined is the runtime's default.
maximumFractionDigitsnumberThe 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

FormatDate props
PropTypeDefaultDescription
valuerequiredFormatDateValueThe date: a Date, a timestamp, or an ISO 8601 string ("2026-10-14T09:30:00Z", or "2026-10-14" for a day).
calendarstring
dateStyle"full" | "short" | "long" | "medium"
day"numeric" | "2-digit"
dayPeriod"short" | "long" | "narrow"
era"short" | "long" | "narrow"
formatMatcher"best fit" | "basic"
fractionalSecondDigits1 | 2 | 3
hour"numeric" | "2-digit"
hour12boolean
hourCycle"h11" | "h12" | "h23" | "h24"
localestringBCP 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"
numberingSystemstring
second"numeric" | "2-digit"
timeStyle"full" | "short" | "long" | "medium"
timeZonestringThe 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

FormatRelativeTime props
PropTypeDefaultDescription
valuerequiredFormatDateValueThe date: a Date, a timestamp, or an ISO 8601 string.
livebooleanfalseWrites the time again every minute, against the current time, while it is shown. Ignored when now is given.
localestringBCP 47 locale ("de-DE"); undefined is the runtime's default.
nowFormatDateValueThe 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.

Theme tokens
TokenUsed for