# Timestamp

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

- Group: Display
- Import: `import { Timestamp } from '@misoto22/design'`
- Page: https://ui.misoto22.com/components/timestamp/
- Related: text, badge, description-list

## 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.

## Anatomy

- **Element** (required) — A <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 paint** (required) — The 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 reading** (required) — Applied 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 value** — A 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.

## 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.

## Timestamp

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.

### Props

- `value` (required) — `Date | string | number`. The instant. A `Date`, an ISO string, or epoch milliseconds.
- `format` — `TimestampFormat` default `'auto'`. How the instant reads. See TimestampFormat.
- `relativeWithin` — `number` default `604_800_000`. How far from now `auto` still prints a relative time, in milliseconds. Seven days by default — past a week the calendar date is the more useful fact, and it is also the one that stops changing.
- `showTime` — `boolean` default `false`. Adds the clock time to the absolute form.

Also accepts: `Omit<TimeHTMLAttributes<HTMLTimeElement>, 'dateTime' | 'children'>`.

## Example — relative and absolute

```tsx
import { DescriptionList, Timestamp } from '@misoto22/design'

<DescriptionList
  layout="row"
  items={[
    { term: 'Thirty seconds ago', description: <Timestamp value={ago(30_000)} /> },
    { term: 'Ninety seconds ago', description: <Timestamp value={ago(90 * 1000)} /> },
    { term: 'Three hours ago', description: <Timestamp value={ago(3 * HOUR)} /> },
    { term: 'Yesterday', description: <Timestamp value={ago(DAY)} /> },
    {
      term: 'Past auto’s window',
      description: <Timestamp value={ago(30 * DAY)} />,
    },
    {
      term: 'Forced absolute, with the time',
      description: <Timestamp value={ago(5 * MINUTE)} format="absolute" showTime />,
    },
    {
      term: 'A value nothing can parse',
      description: <Timestamp value="tomorrow-ish" />,
    },
  ]}
/>
```

## Example — a column of records

```tsx
import { Badge, TBody, TD, TH, THead, TR, Table, Timestamp } from '@misoto22/design'

<Table caption="Recent deploys">
  <THead>
    <TR>
      <TH>Commit</TH>
      <TH>Finished</TH>
      <TH align="end">Outcome</TH>
    </TR>
  </THead>
  <TBody>
    {DEPLOYS.map((deploy) => (
      <TR key={deploy.sha}>
        <TD className="font-mono text-xs">{deploy.sha}</TD>
        <TD>
          <Timestamp value={deploy.at} />
        </TD>
        <TD align="end">
          <Badge tone={deploy.state}>{deploy.label}</Badge>
        </TD>
      </TR>
    ))}
  </TBody>
</Table>
```

## Example — what the markup carries

```tsx
import { Code, DescriptionList, Timestamp } from '@misoto22/design'

<DescriptionList
  items={[
    { term: 'The value passed in', description: <Code>{AT}</Code> },
    { term: 'auto, past its window', description: <Timestamp value={AT} /> },
    { term: 'absolute, with the time', description: <Timestamp value={AT} format="absolute" showTime /> },
    { term: 'relative, however old', description: <Timestamp value={AT} format="relative" /> },
  ]}
/>
```
