UIPackage
Menu

Framework

Change language

Boilerplate repo

Relative Time

relative-time ui
Boilerplate repo

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

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/relative-time.json
Named registry: npx shadcn-vue@latest add @uipkge/relative-time Installs to: app/components/ui/relative-time/

Examples

Loading interactive previews…

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