Skip to content
misoto22 design

Display

Timestamp

A date or a time, rendered the one way the system renders them.

Usage

When to reach for it

Any instant on screen. The alternative is toLocaleString() at the call site, which is how a product ends up with four date formats on one screen.
TSX
import { Timestamp } from '@misoto22/design'

Notes

A date or a time, rendered the one way the system renders them.

Every list of records needs this, and new Date().toLocaleString() at the call site is precisely how a product ends up with four date formats on one screen.

On hydration. Both halves of a formatted date are environment-dependent: a relative time depends on when it is read, and even an absolute one depends on the reader's locale and time zone, none of which a static build knows. So the first paint — the one the server produces and the one the client must reproduce exactly — is the ISO calendar date in UTC, sliced straight out of the ISO string with no Intl anywhere near it. Both sides compute it from the same characters, so they cannot disagree. The locale-aware and relative forms are applied after mount, in an effect, where there is a reader to be local to. This package statically exports every page, so the alternative is a hydration mismatch on any page with a date on it.

The datetime attribute is the full ISO instant from the first render onwards and never changes, so a screen reader, a crawler, or anything else parsing the markup gets the exact moment whether or not the effect has run.

It formats once per mount. A hundred rows each holding a ticking interval to keep "3 minutes ago" honest is a cost nobody asked for; a list that must tick should re-key or re-render from above.

Anatomy

Timestamp anatomy
ElementDescription
ElementrequiredA <time> whose datetime is the full ISO instant from the very first render and never changes, so anything parsing the markup gets the exact moment whether or not the effect has run.
First paintrequiredThe UTC calendar date, sliced straight out of the ISO string with no Intl anywhere near it. It is what the server renders and what the client renders while hydrating — both sides compute it from the same characters, so they cannot disagree.
Local readingrequiredApplied after mount, in an effect, where there is a reader to be local to: Intl.RelativeTimeFormat with numeric "auto" for the relative form, Intl.DateTimeFormat at dateStyle medium for the absolute one.
Missing valueA value nothing can parse renders an em dash at --ink-3-aa and no <time> at all, because an element whose datetime cannot be written is not a time.

Best practices

Do

  • Leave format on auto for a record list. It reads relative while the gap is under relativeWithin — a week by default — and switches to the calendar date past it, which is both the more useful fact and the one that stops changing.
  • Pass the instant, not a formatted string. A Date, an ISO string or epoch milliseconds all work, and all three end up as the same ISO datetime attribute.
  • Re-render from above when a list has to tick. It formats once per mount on purpose: a hundred rows each holding an interval to keep "3 minutes ago" honest is a cost nobody asked for.

Don’t

  • Do not expect the relative text in the server-rendered HTML. The first paint is deliberately the UTC date — a crawler, a static export and a test reading markup all see 2026-01-14, and only a mounted browser sees "3 hours ago".
  • Do not format a date beside it with toLocaleString. The two would disagree the moment one page renders on a build server, which is exactly the hydration mismatch this component is built around.
  • Do not use it for a duration. It renders an instant relative to now; "2m 14s of build time" is a length, not a moment, and belongs in a plain string.

Examples

relative and absolute

The same instant, three ways. Every page here is statically exported, so the first paint — the one the build produces and the browser has to reproduce exactly while hydrating — is the UTC calendar date, sliced out of the ISO string with no Intl involved. The relative and locale-aware forms are applied after mount, where there is a reader to be local to. The datetime attribute is the full ISO instant from the first render and never changes.

Thirty seconds ago
Ninety seconds ago
Three hours ago
Yesterday
Past auto’s window
Forced absolute, with the time
A value nothing can parse

a column of records

The real use, and the reason the component exists rather than a call to toLocaleString at each site. auto is doing the work down this column: the recent rows read as a relative gap and the older ones as calendar dates, and the switch happens at a week, because past that the date is both the more useful fact and the one that stops changing. It formats once per mount — a hundred rows each holding a ticking interval to keep three hours ago honest is a cost nobody asked for, so a list that must tick re-renders from above.

Recent deploys
CommitFinishedOutcome
a1b2c3dDeployed
9f8e7d6Deployed
4c5b6a7Rolled back
77aa2b1Deployed

what the markup carries

The same instant three times, beside the string every one of them is built from. This page is statically exported, so the first paint — the one the build produces and the browser has to reproduce exactly while hydrating — is the UTC calendar date sliced straight out of that string, with no Intl anywhere near it: both sides compute it from the same characters, so they cannot disagree. The locale-aware and relative forms are applied after mount, where there is a reader to be local to. The datetime attribute is the full instant from the first render onwards and never changes, which is what a screen reader or a crawler reads whether or not the effect has run.

The value passed in
2026-01-14T09:30:00.000Z
auto, past its window
absolute, with the time
relative, however old

Types

TSX
export type TimestampFormat = 'auto' | 'relative' | 'absolute'

Accessibility

  • The datetime attribute carries the exact ISO instant from the first render, so assistive technology reading the machine value never depends on an effect having run.
  • The visible text changes once after mount and the machine value never does, which keeps the announced value and the parsed value in agreement.
  • An unparseable value renders an em dash rather than the browser’s literal "Invalid Date" string, which is an engineering artefact and not something to put in front of a reader.