Relative time
A timestamp read as a distance from now, refreshing itself on an interval.
Renders a real <time dateTime> element whose title is the absolute date, so the exact moment is always one hover away. formatRelative owns the thresholds: "just now" under 45 seconds, an absolute date from seven days out. Hydration warnings are suppressed on the element, because the server and the client read their clocks a moment apart.
Install
$
npx shadcn@latest add @vibra/relative-timeNeeds the @vibra registry in your components.json — set it up once.
Examples
import { RelativeTime } from "@/components/ui/relative-time"
// A fixed reference instant, not Date.now(): a statically built page freezes
// the server's copy at build time, so a live clock here would read one thing in
// the HTML and another once the reader's own clock takes over. format-demo.tsx
// pins its instant for the same reason.
const NOW = new Date(2026, 8, 4, 12, 0, 0).getTime()
const EVENTS = [
{ id: "evt_7c1a", label: "Deploy succeeded", at: new Date(NOW - 40_000) },
{ id: "evt_7c19", label: "Webhook retried", at: new Date(NOW - 8 * 60_000) },
{ id: "evt_7c18", label: "API key rotated", at: new Date(NOW - 5 * 60 * 60_000) },
{ id: "evt_7c17", label: "Workspace created", at: new Date(NOW - 40 * 24 * 60 * 60_000) },
]
export default function RelativeTimeDemo() {
return (
<div className="w-full max-w-md divide-y rounded-lg border">
{EVENTS.map((event) => (
<div key={event.id} className="flex items-center justify-between gap-4 px-3 py-2">
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm">{event.label}</span>
<span className="font-mono text-xs text-muted-foreground">{event.id}</span>
</div>
<RelativeTime date={event.at} className="shrink-0 text-xs text-muted-foreground" />
</div>
))}
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| date | Date | string | number | — | The moment being described. |
| updateInterval | number | null | 60000 | Milliseconds between re-reads of the clock; null freezes the value. |
| format | "narrow" | "long" | "narrow" | narrow reads "3m ago", long reads "3 minutes ago". |
| addSuffix | boolean | true | Keeps the "ago" and "in" wrappers; false leaves the bare distance. |
Dependencies
Registry
Source
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { useInterval } from "@/hooks/use-interval"
import { formatDate, formatRelative } from "@/lib/format"
const UNIT_WORDS: Record<string, string> = { m: "minute", h: "hour", d: "day" }
// formatRelative owns the thresholds — "just now" under 45 seconds, an
// absolute date from seven days out — so the long form re-reads its output
// rather than keeping a second set of rules that could drift from it.
const NARROW = /^(?:in )?(\d+)([mhd])( ago)?$/
/** Rewrites `formatRelative`'s narrow output into the requested shape, e.g. "3m ago" → "3 minutes ago" or "3m". */
function relativeText(
date: Date | string | number,
now: Date,
format: "narrow" | "long",
addSuffix: boolean
): string {
const base = formatRelative(date, now)
const match = NARROW.exec(base)
// "just now", or the absolute-date fallback: neither takes a suffix.
if (!match) return base
const [, amount, unit, ago] = match
const core =
format === "long"
? `${amount} ${UNIT_WORDS[unit] ?? unit}${amount === "1" ? "" : "s"}`
: `${amount}${unit}`
if (!addSuffix) return core
return ago ? `${core} ago` : `in ${core}`
}
export type RelativeTimeProps = React.ComponentProps<"time"> & {
date: Date | string | number
/**
* What "now" is. Defaults to the machine clock, which is right for a live
* page; a page whose figures are fixed to a reference date passes that date
* instead, so its stamps do not drift away from them as the year turns. When
* it is given, the machine clock plays no part in the stamp and the interval
* that would refresh it does not start.
*/
now?: Date
/** Milliseconds between re-reads of the clock; null freezes the value. */
updateInterval?: number | null
format?: "narrow" | "long"
addSuffix?: boolean
}
function RelativeTime({
className,
date,
now: fixedNow,
updateInterval = 60_000,
format = "narrow",
addSuffix = true,
...props
}: RelativeTimeProps) {
const target = React.useMemo(() => (date instanceof Date ? date : new Date(date)), [date])
const [clock, setClock] = React.useState(() => new Date())
const now = fixedNow ?? clock
useInterval(() => setClock(new Date()), fixedNow ? null : updateInterval)
return (
<time
data-slot="relative-time"
data-format={format}
dateTime={target.toISOString()}
// The exact moment stays one hover away, whatever the relative text says.
title={formatDate(target, "long")}
// The server and the client read their clocks a moment apart.
suppressHydrationWarning
className={cn("tabular-nums", className)}
{...props}
>
{relativeText(target, now, format, addSuffix)}
</time>
)
}
export { RelativeTime }