# Dates and times

_Box Kite 2.2.6 · a markdown copy of https://www.box-kite.dev/dates/_

Dates, times and time zones with no dependency: every value is an ISO string, every zone comes from Intl, and every control hands back UTC and the reader's own clock beside the value.

**Import**

```jsx
import { format, toUtc, toLocal, today, parse } from '@box-kite/react/date';
```

## A value's shape is its kind

A date in JavaScript is a `Date`, and a `Date` is always a moment in the runtime's zone — which is how a birthday saved in Chișinău comes back as the day before in New York. This layer never holds one. Every value is an ISO string, and what the string looks like is what it means:

date

`2026-10-19`

A day with no time and no zone: a birthday, a due date. The same day everywhere.

time

`14:30:00`

A time of day with no date: opening hours, an alarm.

datetime

`2026-10-19T14:30:00`

A wall clock with no zone: a train timetable, a meeting "at nine wherever you are".

instant

`2026-10-19T14:30:00+03:00`

One moment, written as one zone sees it — or `…Z` in UTC. Anything stored or sent.

A string is what a form posts, what JSON carries and what a Server Component can hand a client one, so a value never needs converting at a boundary — only when the app asks a question of it. Temporal, which ships in Chrome and Firefox and not yet in Safari, draws the same four lines; the names here are its names.

## Conversions say what they mean

Every conversion names its target. `toUtc` is the moment in UTC, `toLocal` the reader's wall clock, `toZone` another one, and `toPlainDate` the day a moment falls on _somewhere_ — which is the question a `Date` answers wrong.

```jsx
toUtc('2026-10-19T14:30', { from: 'Europe/Chisinau' }); // '2026-10-19T11:30:00Z'
toZone('2026-10-19T11:30:00Z', 'America/New_York');   // '2026-10-19T07:30:00-04:00'
toLocal('2026-10-19T11:30:00Z');                       // the reader's wall clock, with its offset
toPlainDate('2026-10-19T23:30:00Z', { zone: 'utc' });  // '2026-10-19' — and '2026-10-20' in Tokyo
```

## Zones, offsets and the hour that does not exist

A zone is `'local'` (the reader), `'utc'`, an IANA name or a fixed offset like `'+03:00'`, and every one of them is read through `Intl` — so there is no zone database to ship and no update to wait for. The offsets are cached per zone, one formatter each: about 4µs a lookup.

Twice a year a wall clock skips an hour or repeats one, and a time inside it names no moment or two. The layer answers the way Temporal does: `disambiguation: 'compatible'` by default (the later reading of a skipped time, the earlier of a repeated one, which is what every operating system does), with `'earlier'`, `'later'` and `'reject'` beside it.

```jsx
toInstant('2026-03-29T02:30', { zone: 'Europe/Berlin' });                  // '2026-03-29T03:30:00+02:00'
toInstant('2026-10-25T02:30', { zone: 'Europe/Berlin', disambiguation: 'later' }); // '…+01:00'
add('2026-03-28T12:00:00+01:00', { days: 1 }, { zone: 'Europe/Berlin' });        // noon again, '…+02:00'
```

## Every locale, nothing shipped

Names, orders, the first day of the week and the 12- or 24-hour clock all come from the runtime's `Intl`, so the library carries no locale data at all — Romanian, Japanese and Arabic cost the same as English. `format` takes presets rather than tokens, and `parse` reads text in the locale's own order:

```jsx
format('2026-10-19', { locale: 'ro-MD', dateStyle: 'long' }); // '19 octombrie 2026'
format('14:30', { locale: 'en-US' });                         // '2:30 PM'
formatRelative('2026-10-20', { now: '2026-10-19T12:00:00Z' });  // 'tomorrow'
parse('03/04/2026', { locale: 'en-US' });                     // '2026-03-04'
parse('03/04/2026', { locale: 'en-GB' });                     // '2026-04-03'
parse('19 oct 2026 14:30', { locale: 'ro-MD' });              // '2026-10-19T14:30:00'
```

## Writing a value out: DateTime

`<DateTime>` writes a value for whoever is reading, inside a real `<time datetime>` that keeps the exact value for a machine. Order placed Oct 19, 2026, 11:30 AM — on your own wall clock.

```jsx
<DateTime value="2026-10-19T11:30:00Z" />
<DateTime value="2026-10-19T11:30:00Z" zone="utc" format={{ timeStyle: 'short', timeZoneName: 'short' }} />
<DateTime value="2026-10-19" format={{ dateStyle: 'full' }} />
```

## On a server

A server does not know who is reading, and its own zone is usually UTC. So `'local'` on a server is whatever `configureDates({ timeZone })` or a `DateProvider` says the reader is, and the server's own zone is always `systemZone()` — next to UTC, never instead of it.

No hydration error, ever

A date component on a server and while hydrating renders in the fallback — `UTC` and `en-US`, or what a `DateProvider` above it says — and the render after hydration switches to the reader's zone and language. Two renders that disagree are a hydration error, and this is the one way they cannot.

Right on the first paint

The browser writes its zone into a cookie with `zoneCookie()`; the next request's server reads it back with `readZoneCookie` and hands it to a `DateProvider`, and the server's HTML is already in the reader's zone.

```jsx
// In the browser, once:
document.cookie = zoneCookie();

// On the server (a Next.js layout):
const zone = readZoneCookie((await headers()).get('cookie'));
<DateProvider timeZone={zone} locale="ro-MD">{children}</DateProvider>
```

In a Server Component with nothing to hydrate, `format()` is the same text with no component at all.

## Arithmetic

`add` and `subtract` keep a value's kind. The calendar half of a duration (years to days) moves the wall clock, so a day later is the same time tomorrow across a daylight-saving change; the clock half (hours down) moves the moment. `startOf`, `endOf`, `difference`, `compare` and `isSame` take a unit — and `'week'` starts where the reader's locale starts it.

```jsx
add('2026-01-31', { months: 1 });                       // '2026-02-28'
add('23:30', { minutes: 45 });                          // '00:15:00'
startOf('2026-10-21', 'week', { locale: 'en-US' });     // '2026-10-18', a Sunday
difference('2026-02-28', '2026-01-31', 'month');        // 0 — not a month yet
isSame('2026-10-19T22:30:00Z', '2026-10-20', 'day');    // in the reader's zone
```

## Every reading at once

`details(value, { zone })` answers every question at once — the canonical string, the moment in UTC, on the zone's wall clock and on the reader's, the offset, a `Date` and the epoch — and it is what every date control hands its `onValueChange` beside the string, so `(value, { utc, local }) => …` is all an app writes.

Small

The whole layer is under 8 KB gzipped with no React and no engine in it — under half of date-fns with its zone add-on, and no locale files to add.

Not a Date

`toDate()` and `fromDate()` are there for an API that insists on one. A date is its midnight in the zone you name, which is the one way a `Date` can hold a day.

Gregorian

Values are on the Gregorian calendar with Latin digits, which is what ISO 8601 and every database speak; a locale's own calendar is used for display only where you pass it in `format`'s options.

## DateTime props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `value`required | `string \| number \| Date \| null \| undefined` | — | What to show: any ISO string, a `Date` or epoch milliseconds. Nothing is rendered for a non-value. |
| `zone` | `ZoneInput` | — | Whose wall clock an instant is shown on. Default `'local'`, the reader's. A date or a time has no zone. |
| `locale` | `string` | — | The reader's language. Default the reader's own. |
| `format` | `Omit<FormatOptions, 'locale' \| 'zone'>` | — | How to write it — `{ dateStyle: 'long' }`, `{ month: 'short', day: 'numeric' }`. Default by kind. |
| `relative` | `boolean \| Omit<RelativeOptions, 'locale' \| 'zone'>` | — | `3 minutes ago`, `tomorrow` instead of the date. An object passes `now`, `numeric` and `style`. |

## DateTime accessibility

- The text is what is read out; `datetime` carries the canonical value for anything that parses the page.

Swept with axe on every release, in this state: `DateTime`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

