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 Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/relative-time.json
Named registry: npx shadcn@latest add @uipkge-react/relative-time Installs to: 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.

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