Components

Time

Dates show up everywhere in your work — table cells, record heroes, timelines, KeyValue rows — and this is the one component that renders all of them. A semantic <time> element with a machine-readable value and an absolute-date tooltip, on one simple rule: dates live in state as UTC and only get formatted here, at the display layer (ADR-0033).

It formats in your language with a pinned US region (en-US / es-US) — never the browser locale — in your time zone, and it never lets a calendar date slip across zones. The friendly text can be as short as "3 days ago"; the exact moment is always one hover (and one screen-reader stop) away.

Modes

Four modes, one element. Every value below is a full UTC timestamp — hover any of them and the precise date and time pops right up (it's also the machine-readable value). The output is computed live, so the relative ones keep moving as time goes by.

mode: date
The absolute date, spelled month — unambiguous. The default for fixed dates.
mode: datetime
Date + time of day, in the viewer's time zone.
mode: relative
Always relative — "3 days ago" / "hace 3 días" (Intl.RelativeTimeFormat).
mode: when (recent)
Within 7 days → relative.
mode: when (older)
Beyond 7 days → the absolute date. The smart default.

The smart "when"

The default mode is "when", and it does the thinking for you: fresh timestamps read relative ("updated 2 hours ago" — easy to scan), and once they pass the 7-day mark they switch to the plain absolute date. Either way the full instant stays in the tooltip and the machine-readable value, so nothing gets lost. Hover any date below and see for yourself.

The policy

One source of truth, three axes, three sources. Everything goes through it — no ad-hoc date formatting, anywhere.

language
The user's in-app locale (useLocale()), not the browser — so dates stay in sync with the rest of the translated UI, consistent across a team, and auditable.
region
Pinned to REGION = 'US' (bilingual US operators) — a Spanish speaker still reads US conventions. One constant to change when a market is added.
time zone
The viewer's zone by default; a timeZone override is reserved for a future org / jurisdiction zone. Calendar dates are never shifted (no off-by-one).
store / display
Instants live in state as UTC; calendar dates as tz-agnostic YYYY-MM-DD. Localization happens only at this layer.

Accessibility

  • A real <time> element. The raw ISO is the machine-readable dateTime; assistive tech and tooling get the precise value.
  • Terse never costs precision. The full absolute date is always the title, so "3 days ago" still exposes the exact instant.
  • No hydration fl… time-zone and relative output resolve on the client; the server renders a stable absolute fallback, so there is no hydration mismatch (and the console stays clean).

API

<Time value mode? timeZone? live?>
The render primitive. value is an ISO instant or YYYY-MM-DD; mode defaults to "when"; live ticks relative times every 30s.
useDateFormatter(timeZone?)
The same policy bound to the user's locale, for formatting outside JSX (a toast string, a derived label): .date / .dateTime / .relative / .when / .absolute.
formatDate / formatDateTime / timeAgo / formatWhen
The pure, framework-free functions underneath — take an explicit locale + timeZone.
DataTable meta.format
A date column auto-renders through <Time> with meta: { format: 'when' } — no per-column cell code.

Usage

Render a date in JSXtsx
import { Time } from '@halo-compliance/ui'

<Time value={officer.updatedAt} />            {/* smart "when" */}
<Time value={report.effectiveDate} mode="date" />
<Time value={event.at} mode="relative" live />
A DataTable date columntsx
{
  accessorKey: 'updatedAt',
  header: 'Updated',
  meta: { format: 'when' },   // 'date' | 'datetime' | 'relative' | 'when'
}
Format outside JSXtsx
import { useDateFormatter } from '@halo-compliance/ui'

const fmt = useDateFormatter()
toast.affirm(`Saved at ${fmt.dateTime(savedAt)}`)

When to use

  • Every rendered date. If a date reaches the screen, it goes through Time (or useDateFormatter) — never toLocaleDateString by hand.
  • relative / when for activity. "Last updated", audit events, anything where recency is the point.
  • date for fixed facts. Effective dates, birthdates, a created date you want stable — absolute, not ticking.

Anti-patterns

  • Formatting at the source. Keep state in UTC / ISO; format only here. A localized string in state is a bug waiting to be compared or stored.
  • new Date('2026-01-14'). Parsing a calendar date as UTC midnight then showing it local is the classic off-by-one — pass the YYYY-MM-DD to Time and it stays put.
  • Relative with no absolute. "3 days ago" alone is unauditable; Time always keeps the exact date in the tooltip + value.
  • /date-picker — the form-bound pickers that capture dates (and store the ISO Time renders).
  • /data-table — date columns format through meta.format.
  • /collection — the live list view using it.
  • ADR-0033 — ADR-0033, the full policy + the alternatives weighed.