Relative Time
relative-time ui Live relative timestamp — "2 minutes ago", "yesterday", "in 3 days" — via Intl.RelativeTimeFormat. `display` switches the label to relative, absolute, or both. `timeZone` (IANA or `UTC`) formats the clock and tooltip; omit it for the browser local zone. `parseAs` treats naive ISO strings as local or UTC. Renders a semantic <time>. Pass `now` to freeze the clock for tests and SSR.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/relative-time.json $ npx shadcn@latest add https://uipkge.dev/r/react/relative-time.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/relative-time.json $ bunx shadcn@latest add https://uipkge.dev/r/react/relative-time.json npx shadcn@latest add @uipkge-react/relative-time Installs to: components/ui/relative-time/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
date Instant to display. Accepts a Date, ISO string, or epoch ms. | Date | string | number | — | required |
now Clock used for the delta. Pass in tests and SSR to keep output stable. | Date | string | number | — | optional |
formatStyle Intl relative style. | RelativeTimeStyle | — | optional |
numeric `auto` yields "yesterday"; `always` yields "1 day ago". | RelativeTimeNumeric | — | optional |
locale BCP 47 locale. Defaults to the runtime locale. | string | — | optional |
display Visible label: relative (default), absolute clock, or both. | RelativeTimeDisplay | — | optional |
timeZone IANA zone for absolute text and the title tooltip. Omit for the browser local zone. Pass `UTC` for UTC. | string | — | optional |
parseAs How to parse date strings with no offset. `local` is JS default; `utc` treats naive ISO as UTC. | RelativeTimeParseAs | — | optional |
updateInterval Tick interval in ms. `0` freezes the clock. | number | — | optional |
Used by
Files installed (3)
-
components/ui/relative-time/relative-time.tsx 2.7 kB
import * as React from 'react' import { cn } from '@/lib/utils' import { formatAbsoluteTime, formatVisibleTime, toDate, type RelativeTimeDisplay, type RelativeTimeNumeric, type RelativeTimeParseAs, type RelativeTimeStyle, } from './format-relative-time' export interface RelativeTimeProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime'> { /** Instant to display. Accepts a Date, ISO string, or epoch ms. */ date: Date | string | number /** Clock used for the delta. Pass in tests and SSR to keep output stable. */ now?: Date | string | number /** Intl relative style. */ formatStyle?: RelativeTimeStyle /** `auto` yields "yesterday"; `always` yields "1 day ago". */ numeric?: RelativeTimeNumeric /** BCP 47 locale. Defaults to the runtime locale. */ locale?: string /** Visible label: relative (default), absolute clock, or both. */ display?: RelativeTimeDisplay /** IANA zone for absolute text and the title tooltip. Omit for the browser local zone. Pass `UTC` for UTC. */ timeZone?: string /** How to parse date strings with no offset. `local` is JS default; `utc` treats naive ISO as UTC. */ parseAs?: RelativeTimeParseAs /** Tick interval in ms. `0` freezes the clock. */ updateInterval?: number } const RelativeTime = React.forwardRef<HTMLTimeElement, RelativeTimeProps>( ( { date, now, formatStyle = 'long', numeric = 'auto', locale, display = 'relative', timeZone, parseAs = 'local', updateInterval = 30_000, className, children, ...props }, ref, ) => { const [tick, setTick] = React.useState(0) React.useEffect(() => { if (updateInterval <= 0 || now !== undefined) return const timer = setInterval(() => setTick((n) => n + 1), updateInterval) return () => clearInterval(timer) }, [updateInterval, now]) const resolvedDate = toDate(date, parseAs) const resolvedNow = now === undefined ? new Date() : toDate(now, parseAs) void tick const label = formatVisibleTime(resolvedDate, resolvedNow, { display, style: formatStyle, numeric, locale, timeZone, }) const absolute = formatAbsoluteTime(resolvedDate, locale, timeZone) return ( <time ref={ref} data-uipkge="" data-slot="relative-time" data-display={display} data-timezone={timeZone || 'local'} data-parse-as={parseAs} dateTime={resolvedDate.toISOString()} title={absolute} className={cn('text-muted-foreground text-sm tabular-nums', className)} {...props} > {children ?? label} </time> ) }, ) RelativeTime.displayName = 'RelativeTime' export { RelativeTime } -
components/ui/relative-time/format-relative-time.ts 2.3 kB
export type RelativeTimeStyle = 'long' | 'short' | 'narrow' export type RelativeTimeNumeric = 'always' | 'auto' export type RelativeTimeParseAs = 'local' | 'utc' export type RelativeTimeDisplay = 'relative' | 'absolute' | 'both' const DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [ { amount: 60, unit: 'second' }, { amount: 60, unit: 'minute' }, { amount: 24, unit: 'hour' }, { amount: 7, unit: 'day' }, { amount: 4.34524, unit: 'week' }, { amount: 12, unit: 'month' }, { amount: Number.POSITIVE_INFINITY, unit: 'year' }, ] const NAIVE_ISO = /^(\d{4}-\d{2}-\d{2})(?:T(\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?$/ export function toDate(value: Date | string | number, parseAs: RelativeTimeParseAs = 'local'): Date { if (value instanceof Date) return value if (typeof value === 'number') return new Date(value) if (parseAs === 'utc') { const naive = value.match(NAIVE_ISO) if (naive) return new Date(`${naive[1]}T${naive[2] ?? '00:00:00.000'}Z`) } return new Date(value) } export function formatRelativeTime( date: Date, now: Date, options?: { style?: RelativeTimeStyle; numeric?: RelativeTimeNumeric; locale?: string }, ): string { const rtf = new Intl.RelativeTimeFormat(options?.locale, { numeric: options?.numeric ?? 'auto', style: options?.style ?? 'long', }) let duration = (date.getTime() - now.getTime()) / 1000 for (const division of DIVISIONS) { if (Math.abs(duration) < division.amount) { return rtf.format(Math.round(duration), division.unit) } duration /= division.amount } return rtf.format(0, 'second') } export function formatAbsoluteTime(date: Date, locale?: string, timeZone?: string): string { return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short', timeZone, }).format(date) } export function formatVisibleTime( date: Date, now: Date, options?: { display?: RelativeTimeDisplay style?: RelativeTimeStyle numeric?: RelativeTimeNumeric locale?: string timeZone?: string }, ): string { const relative = formatRelativeTime(date, now, options) if ((options?.display ?? 'relative') === 'relative') return relative const absolute = formatAbsoluteTime(date, options?.locale, options?.timeZone) if (options?.display === 'absolute') return absolute return `${relative} · ${absolute}` } -
components/ui/relative-time/index.ts 0.3 kB
export { RelativeTime, type RelativeTimeProps } from './relative-time' export { formatAbsoluteTime, formatRelativeTime, formatVisibleTime, toDate, type RelativeTimeDisplay, type RelativeTimeNumeric, type RelativeTimeParseAs, type RelativeTimeStyle, } from './format-relative-time'
Raw manifest: https://uipkge.dev/r/react/relative-time.json