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 React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/relative-time.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/relative-time.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/relative-time.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/relative-time.json npx shadcn-vue@latest add @uipkge/relative-time Installs to: app/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. 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 |
locale BCP 47 locale. Defaults to the runtime locale. | string | — | optional |
display Visible label: relative (default), absolute clock, or both. | RelativeTimeDisplay | 'relative' | 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 | 'local' | optional |
updateInterval Tick interval in ms. `0` freezes the clock. | number | 30_000 | optional |
class | HTMLAttributes['class'] | — | optional |
Used by
Files installed (3)
-
app/components/ui/relative-time/RelativeTime.vue 3 kB
<script setup lang="ts"> import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { formatAbsoluteTime, formatVisibleTime, toDate, type RelativeTimeDisplay, type RelativeTimeNumeric, type RelativeTimeParseAs, type RelativeTimeStyle, } from './format-relative-time' const props = withDefaults( defineProps<{ /** 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 class?: HTMLAttributes['class'] }>(), { formatStyle: 'long', numeric: 'auto', display: 'relative', parseAs: 'local', updateInterval: 30_000, }, ) const tick = ref(0) let timer: ReturnType<typeof setInterval> | null = null const resolvedNow = computed(() => { tick.value return props.now === undefined ? new Date() : toDate(props.now, props.parseAs) }) const resolvedDate = computed(() => toDate(props.date, props.parseAs)) const label = computed(() => formatVisibleTime(resolvedDate.value, resolvedNow.value, { display: props.display, style: props.formatStyle, numeric: props.numeric, locale: props.locale, timeZone: props.timeZone, }), ) const absolute = computed(() => formatAbsoluteTime(resolvedDate.value, props.locale, props.timeZone)) const iso = computed(() => resolvedDate.value.toISOString()) function stop() { if (timer) { clearInterval(timer) timer = null } } function start() { stop() if (props.updateInterval <= 0 || props.now !== undefined) return timer = setInterval(() => { tick.value += 1 }, props.updateInterval) } onMounted(start) watch(() => [props.updateInterval, props.now], start) onBeforeUnmount(stop) </script> <template> <time 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', props.class)" > <slot :label="label" :absolute="absolute">{{ label }}</slot> </time> </template> -
app/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}` } -
app/components/ui/relative-time/index.ts 0.3 kB
export { default as RelativeTime } from './RelativeTime.vue' export { formatAbsoluteTime, formatRelativeTime, formatVisibleTime, toDate, type RelativeTimeDisplay, type RelativeTimeNumeric, type RelativeTimeParseAs, type RelativeTimeStyle, } from './format-relative-time'
Raw manifest: https://uipkge.dev/r/vue/relative-time.json