---
id: "packages/active-format/presentation"
title: "Presentation"
description: "The one input of every human-readable formatter. A language tag, or a context composed from language, notation profile and time zone."
url: "/packages/active-format/presentation"
locale: "en"
revision: "c7a85cffdb3391f8"
---

# Presentation

Every human-readable function takes a `presentation`. There are two forms:

- **A bare language tag** (`LocaleCode`), such as `'en'`, `'nl'` or `'pt-BR'`. Notation follows the
  language: its own date order, separators and clock.
- **A composed context** (`PresentationContext`), from
  `composePresentationContext(language, profile, timeZone)`. Use it when a reader has preferences
  that differ from their language, or when instants must be shown in a specific zone.

`composePresentationContext` is **the only place** language, profile and zone meet. Compose once,
where you know the reader, and pass the result down unchanged.

## Change the presentation, see every formatter follow

**Example: One presentation for every formatter**

Client: Runs entirely in your browser. Source: `examples/format/presentation-switcher.tsx`.

```tsx
// 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>
  );
}
```

## The profile

A profile (`PresentationProfileConfig`) is a set of optional notation choices. **An absent field
means "not configured"**: the language's own convention applies, never a hidden default.

Fields of `PresentationProfileConfig`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `dateShape` | `"LONG_TEXTUAL" \| "NUMERIC" \| "SHORT_TEXTUAL"` | no | The normal human date shape (PP-053): the shape an ordinary human date consumer inherits. Subordinate to it, and not the other way around, is {@link dateFormat} (PP-055). |
| `dateFormat` | `"DMY_NUMERIC" \| "ISO_NUMERIC" \| "MDY_NUMERIC"` | no | Structural date notation within the numeric shape (PP-016). |
| `timeFormat` | `"H12" \| "H24"` | no | Clock notation, independent of the date order (PP-019). |
| `numberFormat` | `"COMMA_DECIMAL_DOT_GROUP" \| "COMMA_DECIMAL_SPACE_GROUP" \| "DOT_DECIMAL_COMMA_GROUP"` | no | Number notation family (PP-017). |
| `currencyDisplay` | `"CODE" \| "SYMBOL"` | no | Currency display style (PP-008). |

The values come from the package's vocabulary, exported as runtime lists so a settings screen can
offer them:

| Field | Values | List |
| --- | --- | --- |
| `dateShape` | `NUMERIC`, `SHORT_TEXTUAL`, `LONG_TEXTUAL` | `DATE_SHAPE_PRESETS` |
| `dateFormat` | `DMY_NUMERIC`, `MDY_NUMERIC`, `ISO_NUMERIC` | `DATE_NOTATION_PRESETS` |
| `timeFormat` | `H24`, `H12` | `TIME_NOTATION_PRESETS` |
| `numberFormat` | `DOT_DECIMAL_COMMA_GROUP`, `COMMA_DECIMAL_DOT_GROUP`, `COMMA_DECIMAL_SPACE_GROUP` | `NUMBER_NOTATION_PRESETS` |
| `currencyDisplay` | `SYMBOL`, `CODE` | `CURRENCY_DISPLAY_PRESETS` |

Which value applies to which reader is your policy: the package only names the choices.

## Passing it through your own components

A component or package that formats values should **not** choose a language itself. Give it a
`presentation` (or `locale`) prop of type `ValuePresentation` and pass the value on to every
formatter it calls. Replace it as a whole; never merge single fields into it.

```tsx
import {formatCurrency, type ValuePresentation} from '@workspace/active-format';

export function Price({
  amount,
  presentation,
}: {
  amount: number;
  presentation: ValuePresentation;
}) {
  return <span>{formatCurrency({amount, currency: 'EUR'}, presentation)}</span>;
}
```

## Words versus notation

Words (month names, "and", unit names) follow the reader's **language**. Numbers inside them follow
the presentation's **notation**: a `numberFormat` choice rewrites separators and digits, never the
words.
