---
id: "packages/active-format/dates-and-time-zones"
title: "Dates and time zones"
description: "Calendar days, instants, presets, time zones, relative time and machine forms."
url: "/packages/active-format/dates-and-time-zones"
locale: "en"
revision: "c3a57babd6a41c78"
---

# Dates and time zones

## Two kinds of dates

The package keeps two things apart that look alike:

- A **calendar day** (`YYYY-MM-DD`): a birthday, a due date, a holiday. It has no time and no zone,
  and it reads the same everywhere. Format it with `formatDateOnly`.
- An **instant**: a moment in time, written with `Z` or an offset (`2026-03-09T23:30:00Z`), a
  `Date`, or epoch milliseconds. It reads differently per zone. Format it with `formatDate` or
  `formatDateTime`, in the presentation's time zone.

A date-time **without** a zone (`2026-03-09T23:30:00`) is neither: it gives `''`, never a guess.

**Example: Calendar day versus instant**

Client: Runs entirely in your browser. Source: `examples/format/calendar-day-vs-instant.tsx`.

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

`calendarDayOf(instant, timeZone)` gives the calendar day of an instant in a zone. An instant
without a zone argument throws, so the zone is never the runtime's by accident.

## Time zones

Time zones are `IanaTimeZone` values: a validated IANA identity such as `'Europe/Amsterdam'`, never
an offset. Make one with `toIanaTimeZone` (throws for an unknown zone) or test a string with
`isIanaTimeZone`. `UTC_TIME_ZONE` is `'UTC'`.

The zone belongs in the presentation: `composePresentationContext(language, profile, timeZone)`.
Without a zone, instants are formatted in the runtime zone.

## Presets

A preset names a form; the language and profile decide its exact pattern.

- Calendar-day presets (`DateOnlyPreset`): `numeric`, `short`, `long`, `full`, `day-month`,
  `day-month-numeric`, `day-month-year-short`, `month-year`, `quarter-year`, `month`, `month-long`,
  `weekday-long`, `year`.
- Time and date-time presets (`DatePreset` adds): `time`, `time-seconds`, `time-millis`,
  `numeric-datetime`, `numeric-datetime-seconds`, `numeric-datetime-millis`, `datetime`,
  `datetime-long`, `time-day`.

Without a preset, the form follows the presentation's date shape (`normalDateOnlyPreset`,
`normalDateTimePreset`). Language-derived notations are day-month-year with a 24-hour clock; a
month-first or 12-hour form only comes from a profile (`MDY_NUMERIC`, `H12`).

`formatDateWithPattern(value, pattern, presentation, reason)` is the escape hatch for a date-fns
pattern. The `reason` argument is required, so every use explains itself.

## Today

`todayIsoDate(timeZone)` gives today's calendar day in an explicit zone, never the runtime's day.
It is one of only two functions that read the clock.

## Relative time

`resolveRecency(value, presentation, {now?, thresholdDays?, absolutePreset?})` is the only route to
relative time. Within the threshold (seven days by default, in both directions) it is relative
("2 minutes ago", "in 3 days"), beyond it absolute. Pass `now` and it reads no clock.

**Example: Relative time**

Client: Runs entirely in your browser. Source: `examples/format/recency.tsx`.

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

## Machine forms

For files, APIs and URLs, never for text a person reads:

- `formatMachineDate(value)`: `yyyy-MM-dd`.
- `formatMachineDateTime(value)`: `YYYY-MM-DDTHH:mm:ss.sssZ`, for an instant only.
- `formatMachineNumber(value, decimals?)`: a plain dot decimal, never an exponent.

Absence gives `''`; an invalid value **throws**, unlike the human formatters.
