Dates and times
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-19A day with no time and no zone: a birthday, a due date. The same day everywhere.
time
14:30:00A time of day with no date: opening hours, an alarm.
datetime
2026-10-19T14:30:00A wall clock with no zone: a train timetable, a meeting "at nine wherever you are".
instant
2026-10-19T14:30:00+03:00One 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 TokyoZones, 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 — 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 zoneEvery 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 rather than repeated here.
| Prop | Type | Default | What it does |
|---|---|---|---|
valuerequired | 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;
datetimecarries 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.