# 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"`
- **Page:** <https://ui.booleanpress.com/components/format> · @booleanpress/ui 0.2.0

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

```tsx
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.

```ts
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.

```tsx
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.

```tsx
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).

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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

| Key | Behaviour |
| --- | --- |

## API

### FormatNumber

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `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 |
| --- | --- | --- | --- |
| `currency` (required) | `string` |  | The ISO 4217 code of the currency: `"USD"`, `"EUR"`. |
| `value` (required) | `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 |
| --- | --- | --- | --- |
| `value` (required) | `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 |
| --- | --- | --- | --- |
| `value` (required) | `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 |
| --- | --- | --- | --- |
| `value` (required) | `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.
