Relative Time
relative-timeuiLive 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.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/relative-time.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/relative-time.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/relative-time.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/relative-time.jsonhttps://uipkge.dev/r/svelte/relative-time.jsonInstalls to:src/lib/components/ui/relative-time/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
dateInstant to display. Accepts a Date, ISO string, or epoch ms. | Date | string | number | — | required |
nowClock used for the delta. Pass in tests and SSR to keep output stable. | Date | string | number | — | optional |
formatStyleIntl relative style. Named `formatStyle` so it does not collide with the HTML style attribute. | RelativeTimeStyle | long | optional |
numeric`auto` yields "yesterday"; `always` yields "1 day ago". | RelativeTimeNumeric | auto | optional |
localeBCP 47 locale. Defaults to the runtime locale. | string | — | optional |
displayVisible label: relative (default), absolute clock, or both. | RelativeTimeDisplay | relative | optional |
timeZoneIANA zone for absolute text and the title tooltip. Omit for the browser local zone. Pass `UTC` for UTC. | string | — | optional |
parseAsHow to parse date strings with no offset. `local` is JS default; `utc` treats naive ISO as UTC. | RelativeTimeParseAs | local | optional |
updateIntervalTick interval in ms. `0` freezes the clock. | number | 30 | optional |
childrenOverride the rendered label. Receives the computed `label` and `absolute` strings. | Snippet<[{ label: string; absolute: string }]> | — | optional |
ref | HTMLTimeElement | null | — | optional |
Used by
Files installed (3)
src/lib/components/ui/relative-time/RelativeTime.svelte3.2 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' import type { RelativeTimeDisplay, RelativeTimeNumeric, RelativeTimeParseAs, RelativeTimeStyle, } from './format-relative-time' // Omit `children`: the snippet carries label/absolute strings, which narrows the base `Snippet` type. export interface RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>, 'children'> { /** 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. Named `formatStyle` so it does not collide with the HTML style attribute. */ 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 /** Override the rendered label. Receives the computed `label` and `absolute` strings. */ children?: Snippet<[{ label: string; absolute: string }]> ref?: HTMLTimeElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { formatAbsoluteTime, formatVisibleTime, toDate, } from './format-relative-time' let { class: className, date, now = undefined, formatStyle = 'long', numeric = 'auto', locale = undefined, display = 'relative', timeZone = undefined, parseAs = 'local', updateInterval = 30_000, children, ref = $bindable(null), ...restProps }: RelativeTimeProps = $props() let tick = $state(0) $effect(() => { if (updateInterval <= 0 || now !== undefined) return const timer = setInterval(() => { tick += 1 }, updateInterval) return () => clearInterval(timer) }) const resolvedNow = $derived.by(() => { tick return now === undefined ? new Date() : toDate(now, parseAs) }) const resolvedDate = $derived(toDate(date, parseAs)) const label = $derived( formatVisibleTime(resolvedDate, resolvedNow, { display, style: formatStyle, numeric, locale, timeZone, }), ) const absolute = $derived(formatAbsoluteTime(resolvedDate, locale, timeZone)) const iso = $derived(resolvedDate.toISOString()) </script> <time bind:this={ref} data-uipkge="" data-slot="relative-time" datetime={iso} title={absolute} data-display={display} data-timezone={timeZone || 'local'} data-parse-as={parseAs} class={cn('text-muted-foreground text-sm tabular-nums', className)} {...restProps} > {#if children} {@render children({ label, absolute })} {:else} {label} {/if} </time>src/lib/components/ui/relative-time/format-relative-time.ts2.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}` }src/lib/components/ui/relative-time/index.ts0.3 kB
export { default as RelativeTime, type RelativeTimeProps } from './RelativeTime.svelte' export { formatAbsoluteTime, formatRelativeTime, formatVisibleTime, toDate, type RelativeTimeDisplay, type RelativeTimeNumeric, type RelativeTimeParseAs, type RelativeTimeStyle, } from './format-relative-time'
Raw manifest:https://uipkge.dev/r/svelte/relative-time.json