Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
Since
0.1.0
Accessibility pattern
native time element

Last updated

Last deployed

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-time

The 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}.

Instant (delay 0)
Delayed 700ms

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.

en-US
de-DE
fr-FR

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.

Updated

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

PropTypeDefaultDescription
date*string | number | DateThe instant to render, relative to now. Accepts a Date, an ISO string, or an epoch-millisecond number.
focusablebooleanWhether 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.
localestring | 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.
nownumberDate.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).
refreshbooleantrueAuto-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.
titlestring | booleantrueReveal 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.
tooltipDelaynumber0How 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

AttributeValues
data-modemirrors 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-visible shows a 2px ring (outline-ring) — never outline: none alone.
  • Color is inherited from context — the component never encodes meaning in color, so the relative string stands on its own.
KeyAction
TabMove focus to the timestamp (when the tooltip is enabled).
EscDismiss 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.

ContractStates tested
Behaviourdefault
Accessibilitylive, status-announcement, semantic-html
Visualdefault

Do / Don't

Do
Use mode='ago' for moments (created/updated timestamps) and mode='day' for calendar-day grouping (activity-feed headers, due dates).
Don't
Reach for a date library or hardcode the formatting — Intl.RelativeTimeFormat is built in and localizes for free via the locale prop.

On this page