Relative Time
Render a date as a human-relative string ("2 hours ago", "yesterday", "in 3 days") with the native Intl.RelativeTimeFormat — no date library.
- Status
- Since
0.1.0- Accessibility pattern
- native time element
Last updated
Install
Add Relative Time from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/relative-timeThe same command installs the registry items it composes: @vegastack/tooltip, @vegastack/truncated-text.
Usage
import { RelativeTime } from "@/components/ui/relative-time";
<RelativeTime date={comment.createdAt} />;RelativeTime renders a semantic <time dateTime> element and self-updates on a
timer while the date is recent. It uses the platform-native Intl.RelativeTimeFormat
— no date-fns or other date dependency.
Examples
Formats and modes
Past and future instants, from seconds to days. Sub-minute deltas collapse to now;
future dates read in ….
Modes
mode="ago" (default) is duration-relative — "2 hours ago", "in 3 days".
mode="day" is calendar-relative — "today", "yesterday", "tomorrow", and an
absolute date ("March 15", "March 15, 2025") once a date is further out.
States
By default the absolute date-time is revealed in a Tooltip on hover or focus. Pass a
string to title for a custom label, or title={false} to drop the tooltip.
Tooltip delay
tooltipDelay (ms) sets how long the pointer must hover before the absolute-date
tooltip opens. The default 0 reveals it instantly; raise it to avoid flashing the
tooltip on a quick pass-through. Ignored when title={false}.
Localization
RelativeTime formats with the platform-native Intl.RelativeTimeFormat, so passing
a BCP-47 locale localizes the string for free — no translation table. Omit locale
to follow the runtime locale.
Live updates
While the date is recent and now is left unset, the component reads the live clock
and refreshes on an adaptive timer (every 10s under an hour old, every minute under a
day, then static). The example below is intentionally live — it seeds a timestamp
30s in the past and climbs as you watch. Pass refresh={false} to opt out.
Determinism
The displayed value depends on the current time. For tests, SSR snapshots, or
Storybook, pass a fixed now (epoch ms) to render deterministically — this also
disables the refresh timer.
<RelativeTime date={ts} now={Date.UTC(2026, 0, 15, 12, 0, 0)} refresh={false} />Playground
Switch the formatting mode and the instant being rendered, then copy the generated JSX.
<RelativeTime date={new Date(Date.now() - 5 * 60_000)} />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
date* | string | number | Date | — | The instant to render, relative to now. Accepts a Date, an ISO string,
or an epoch-millisecond number. |
focusable | boolean | — | Whether the timestamp becomes a tab stop so keyboard users can open the
absolute-date Tooltip. Defaults to true standalone and to whatever a
TruncationFocusProvider sets — false under a grid or list host, where 50
rows would otherwise mean 50 extra tab stops on top of that host's own roving
focus (audit B2-04 / decision D9). The machine-readable dateTime attribute is
unaffected either way. |
locale | string | string[] | — | BCP-47 locale(s) for Intl formatting. Defaults to the runtime locale. |
mode | "ago" | "day" | 'ago' | Formatting mode.
- ago: duration-relative — "2 hours ago", "in 3 days".
- day: calendar-relative — "today", "yesterday", else an absolute date. |
now | number | Date.now() | Reference instant the relative string is measured against, as epoch ms.
Defaults to the live clock (Date.now()); pass a fixed value to render
deterministically (tests, SSR snapshots, storybook). |
refresh | boolean | true | Auto-refresh the displayed value on a timer while the date is recent (faster
near "now", off once it is a day old). Ignored when now is provided. |
title | string | boolean | true | Reveal the absolute date/time in a Tooltip on hover/focus.
- true: a localized full date-time ("March 15, 2025, 2:30 PM").
- a string: your own label.
- false: no tooltip. |
tooltipDelay | number | 0 | How long to wait (ms) before the tooltip opens on hover. 0 reveals the
absolute date instantly. Ignored when title is false. |
unitStyle | "long" | "narrow" | "short" | 'long' | Unit length, mapped to Intl.RelativeTimeFormat's style — 'long' gives
"2 hours ago", 'short' "2 hr. ago", 'narrow' the compact "2h ago"
(dense tables, activity feeds). Applies to mode="day"'s relative words too. |
Data attributes and CSS variables on RelativeTime
| Attribute | Values |
|---|---|
data-mode | mirrors a prop or state value |
data-slot | "relative-time" |
Server rendering
An uncontrolled instance renders the absolute date ("Mar 15, 2025") on the server and
on the hydration render, then swaps to the relative label once the client clock exists.
There is no empty frame and no layout jump.
The reason it cannot simply render the relative label is that "2 hours ago" needs
Date.now(), which the server cannot reproduce — so the two renders would disagree and
React would warn. Deriving the first paint from the target instant alone keeps both sides
byte-identical. Pass now to make the output fully deterministic and skip the swap.
Accessibility
- Renders a semantic
<time dateTime="…">so the machine-readable ISO timestamp is always present for assistive tech, even as the visible text updates. - When the absolute-date Tooltip is enabled, the
<time>is made focusable (tabindex="0") so keyboard users can reveal it;:focus-visibleshows a 2px ring (outline-ring) — neveroutline: nonealone. - Color is inherited from context — the component never encodes meaning in color, so the relative string stands on its own.
| Key | Action |
|---|---|
| Tab | Move focus to the timestamp (when the tooltip is enabled). |
| Esc | Dismiss the open absolute-date tooltip. |
focusable controls whether the timestamp takes a tab stop to open its absolute-date
Tooltip. It defaults to true standalone, and to whatever an enclosing
TruncationFocusProvider sets — false for a grid or list host, where 50 rows would
otherwise mean 50 extra tab stops on top of that host's own roving focus. The
machine-readable dateTime attribute is there either way.
| Contract | States tested |
|---|---|
| Behaviour | default |
| Accessibility | live, status-announcement, semantic-html |
| Visual | default |