Skip to main content
Format timestamps as human-readable relative strings like “2 hours ago” or “in 5 minutes”. Uses Intl.RelativeTimeFormat for proper i18n — zero hand-rolled formatting strings.

formatRelativeTime()

Pure utility function. Accepts Date, ISO string, or epoch milliseconds:
import { formatRelativeTime } from '@vertz/ui';

formatRelativeTime(new Date()); // "now"
formatRelativeTime('2026-03-21T10:00:00Z'); // "2 hours ago"
formatRelativeTime(Date.now() - 86400000); // "yesterday"

// With locale
formatRelativeTime(date, { locale: 'pt-BR' }); // "há 2 horas"

// Force numeric output
formatRelativeTime(date, { numeric: 'always' }); // "1 day ago" instead of "yesterday"

Signature

type DateInput = Date | string | number;

function formatRelativeTime(date: DateInput, options?: FormatRelativeTimeOptions): string;

Options

OptionTypeDefaultDescription
localestringIntl defaultBCP 47 locale tag (e.g., 'en-US', 'pt-BR')
numeric'auto' | 'always''auto''auto' produces “yesterday”, 'always' produces “1 day ago”
nowDatenew Date()Reference time for calculating the difference. Useful for testing.

Thresholds

The function picks the most appropriate unit based on elapsed time:
Elapsed TimeUnitExample
< 10 secondssecond”now”
< 60 secondssecond”30 seconds ago”
< 60 minutesminute”5 minutes ago”
< 24 hourshour”2 hours ago”
< 7 daysday”3 days ago”
< 30 daysweek”2 weeks ago”
< 365 daysmonth”4 months ago”
>= 365 daysyear”2 years ago”
Future dates work the same way: “in 5 minutes”, “in 2 hours”.
All output comes from Intl.RelativeTimeFormat including the “now” case (format(0, 'second')). This means every string is locale-aware — no English-only special cases.

RelativeTime

Auto-updating component that renders a <time> element. The displayed text refreshes automatically using adaptive intervals.
import { RelativeTime } from '@vertz/ui';

// Basic usage
<RelativeTime date={comment.createdAt} />

// With locale
<RelativeTime date={comment.createdAt} locale="pt-BR" />

// Custom update interval
<RelativeTime date={comment.createdAt} updateInterval={30000} />

// With styling
<RelativeTime date={comment.createdAt} className="text-muted" />

Props

PropTypeDefaultDescription
dateDateInput(required)The date to format. Accepts Date, ISO string, or epoch ms.
localestringIntl defaultBCP 47 locale tag
numeric'auto' | 'always''auto'Passed to Intl.RelativeTimeFormat
updateIntervalnumberadaptiveUpdate interval in ms. Overrides the adaptive default.
classNamestringCSS class for the <time> element
titlestring | falseFull formatted dateTooltip on hover. Set to false to disable.

Rendered HTML

<time datetime="2026-03-21T10:00:00.000Z" title="March 21, 2026, 10:00:00 AM"> 2 hours ago </time>
The datetime attribute always contains the ISO string for machine readability. The title attribute defaults to the full formatted date via Intl.DateTimeFormat.

Adaptive Update Intervals

When updateInterval is not set, the component picks a smart interval based on elapsed time:
Elapsed TimeUpdate Interval
< 1 minuteEvery 10 seconds
< 1 hourEvery 1 minute
< 1 dayEvery 1 hour
>= 1 dayNo updates (static)
Old timestamps stop updating entirely — zero timer overhead.
The component uses setTimeout chains (not setInterval) so the interval can adapt as time elapses. A timestamp that was “30 seconds ago” updates every 10s, but once it crosses into minutes, the interval relaxes to every 60s.

SSR Behavior

During SSR, the component renders the static formatted text. The update timer only starts on the client via onMount(). There may be a minor text drift between SSR and client hydration if a threshold boundary is crossed (e.g., SSR renders “4 minutes ago”, client hydrates with “5 minutes ago”). This is acceptable — the datetime attribute is always correct, and the text updates on the next tick.

Types

type DateInput = Date | string | number;

interface FormatRelativeTimeOptions {
  locale?: string;
  numeric?: 'auto' | 'always';
  now?: Date;
}

interface RelativeTimeProps {
  date: DateInput;
  locale?: string;
  numeric?: 'auto' | 'always';
  updateInterval?: number;
  className?: string;
  title?: string | false;
}