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.
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
timeZoneoverride 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
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 />{
accessorKey: 'updatedAt',
header: 'Updated',
meta: { format: 'when' }, // 'date' | 'datetime' | 'relative' | 'when'
}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.
Related
- /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.