relative-time-live
v0.1.0
Published
Self-updating human relative time ("2 minutes ago") that live-ticks, backed by Intl.RelativeTimeFormat. i18n-ready, framework-agnostic with optional React/Vue bindings.
Maintainers
Readme
relative-time-live
Self-updating, human-friendly relative time ("2 minutes ago", "in 3 hours") that live-ticks, backed by the native Intl.RelativeTimeFormat. i18n-ready, framework-agnostic, with optional React and Vue bindings.
- Live updating with a single shared scheduler — many subscriptions, one timer.
- Deterministic & testable — inject
now, never hiddenDate.now()in pure paths. - Dual ESM + CJS builds with full
.d.tstypes. - Tree-shakeable (
sideEffects: false).
npm install relative-time-liveQuick start
import { relativeTime } from "relative-time-live";
relativeTime(Date.now() - 2 * 60_000); // "2 minutes ago"
relativeTime(Date.now() + 3 * 3_600_000); // "in 3 hours"
relativeTime("2026-05-30T12:00:00Z", { now: Date.parse("2026-05-31T12:00:00Z") });
// "yesterday" (numeric: "auto" default)Live updating in the browser:
import { liveRelativeTime } from "relative-time-live";
const stop = liveRelativeTime(someDate, (text) => {
el.textContent = text; // recomputed on an automatic cadence
});
// later:
stop();Accepted date inputs
Everywhere a date is accepted you may pass:
- a
Dateinstance, - a
numberof milliseconds since the Unix epoch, - a
stringparseable byDate.parse(ISO 8601 recommended).
Invalid inputs throw a TypeError.
API
relativeTime(date, options?) => string
Formats date relative to now using Intl.RelativeTimeFormat, auto-selecting the
best unit (seconds → minutes → hours → days → weeks → months → years).
relativeTime(date, {
locale: "en", // BCP-47 locale or array; default: runtime default
style: "long", // "long" | "short" | "narrow"; default "long"
numeric: "auto", // "auto" | "always"; default "auto"
now: 1748000000000, // number | Date | (() => number); default: live clock
round: "round", // "round" | "floor" | "ceil"; default "round"
minUnit: "second", // smallest unit to use; default "second"
maxUnit: "year", // largest unit to use; default "year"
thresholds: { // see below; "just now" handling
justNow: 5, // seconds: |diff| < 5s -> justNowText (set 0 to disable)
justNowText: "just now",
},
});Options
| Option | Type | Default | Notes |
|--------------|-------------------------------------------------|--------------------|-------|
| locale | string \| string[] | runtime default | Passed to Intl.RelativeTimeFormat. |
| style | "long" \| "short" \| "narrow" | "long" | "short" → "2 min. ago", "narrow" → "2m ago" (locale dependent). |
| numeric | "auto" \| "always" | "auto" | "auto" yields "yesterday"/"tomorrow"/"today"; "always" yields "1 day ago". |
| now | number \| Date \| (() => number) | live clock fn | Inject for tests. A function is called each computation. |
| round | "round" \| "floor" \| "ceil" | "round" | How fractional unit values are reduced to integers. |
| minUnit | Unit | "second" | Values smaller than this clamp up to it. |
| maxUnit | Unit | "year" | Values larger than this clamp down to it. |
| thresholds | { justNow?: number; justNowText?: string } | { justNow: 5 } | If |diff| (seconds) < justNow, returns justNowText (localized via locale pack). |
Unit is "second" | "minute" | "hour" | "day" | "week" | "month" | "year".
The justNowText resolves in this order: explicit thresholds.justNowText → the
registered locale pack's justNow string → "just now".
calendar(date, options?) => string
Calendar-style phrasing relative to now:
- same day →
"Today at 3:00 PM" - previous day →
"Yesterday at 3:00 PM" - next day →
"Tomorrow at 3:00 PM" - within the next/previous 6 days → weekday, e.g.
"Monday at 3:00 PM" - otherwise → absolute date fallback, e.g.
"05/24/2026".
calendar(date, {
locale: "en",
now: someNow, // same as relativeTime
timeStyle: "short", // Intl.DateTimeFormat timeStyle for the "at <time>" part
dateStyle: "short", // Intl.DateTimeFormat dateStyle for the fallback
withTime: true, // append " at <time>" to Today/Yesterday/Tomorrow/weekday
formats: { // override any bucket's template; {time}/{weekday} placeholders
sameDay: "Today at {time}",
nextDay: "Tomorrow at {time}",
lastDay: "Yesterday at {time}",
nextWeek: "{weekday} at {time}",
lastWeek: "{weekday} at {time}",
// sameElse uses the dateStyle fallback
},
});Templates and words (Today/Yesterday/Tomorrow) come from the locale pack and
may be overridden per-call via formats.
range(start, end, options?) => string
Humanizes the duration between two dates as a compound string.
range(start, end); // "3 days"
range(start, end, { units: 2 }); // "2 hours 5 minutes"
range(start, end, {
locale: "en",
units: 1, // max number of unit components to include; default 1
minUnit: "second",
maxUnit: "year",
style: "long", // "long" | "short" | "narrow" (Intl.NumberFormat unit style)
separator: " ", // joins components
});Order of start/end does not matter; the magnitude of the duration is used.
nextUpdateDelay(date, options?) => number
Returns the number of milliseconds until the string produced by relativeTime
would next change, given the current now. Used internally by the live scheduler;
exported for advanced use.
nextUpdateDelay(date, { now }); // e.g. 1000 when <1min old, 60000 when <1hr, etc.The cadence:
- diff
< 1 minute→ updates every second (delay ≤ 1000 ms), - diff
< 1 hour→ updates each minute, - diff
< 1 day→ updates each hour, - beyond → updates each day.
The returned delay is the time until the next boundary, never larger than the cadence and never less than ~250 ms.
liveRelativeTime(date, callback, options?) => () => void
Subscribes to live updates. Immediately invokes callback(text) once, then again
each time the displayed string would change, on an automatic, self-correcting
cadence. Returns an unsubscribe function. All subscriptions share one timer via
the global ticker.
const stop = liveRelativeTime(date, (text) => (el.textContent = text), { style: "short" });
stop(); // unsubscribecreateTicker() => Ticker
Creates an independent scheduler for advanced control (e.g. custom timing source or isolation in tests).
interface Ticker {
subscribe(
date: DateInput,
callback: (text: string) => void,
options?: RelativeTimeOptions,
): () => void;
/** Force recompute of all subscriptions now. */
flush(): void;
/** Stop the underlying timer and clear all subscriptions. */
stop(): void;
/** Number of active subscriptions. */
readonly size: number;
}
import { createTicker } from "relative-time-live";
const ticker = createTicker();DOM helper: liveTimes(root?, options?) => () => void
Scans root (default document.body) for elements carrying a datetime attribute
(as on <time datetime="...">) or a data-time attribute, and keeps their
textContent set to the live relative time. Returns a stop function that removes
all subscriptions. New elements are not auto-discovered; call again or re-run after
DOM changes.
<time datetime="2026-05-31T11:58:00Z"></time>
<span data-time="1748000000000"></span>import { liveTimes } from "relative-time-live"; // also at "relative-time-live/dom"
const stop = liveTimes(document.body, { style: "short" });Per-element overrides via attributes: data-style, data-numeric, data-locale.
React
Import from relative-time-live/react. React >=17 is an optional peer dependency.
useRelativeTime(date, options?) => string
import { useRelativeTime } from "relative-time-live/react";
function Ago({ date }: { date: Date }) {
const text = useRelativeTime(date, { style: "short" });
return <span>{text}</span>;
}Re-renders the component on each tick. Uses the shared global ticker.
<RelativeTime value={...} ... />
Renders a semantic <time> element with a machine-readable dateTime attribute and
live-updating text content.
import { RelativeTime } from "relative-time-live/react";
<RelativeTime value={date} style="short" numeric="auto" locale="en" />;
// <time datetime="2026-05-31T11:58:00.000Z">2 minutes ago</time>Props: value (required DateInput) plus all RelativeTimeOptions, plus any
standard <time> attributes (className, title, …). The dateTime attribute is
set automatically and may be overridden.
Vue
Import from relative-time-live/vue. Vue >=3 is an optional peer dependency.
useRelativeTime(date, options?) => Ref<string>
import { useRelativeTime } from "relative-time-live/vue";
const text = useRelativeTime(someDate, { style: "short" });date and options may be plain values, refs, or getters; the composable reacts to
changes and re-subscribes. Cleans up on unmount.
RelativeTime component
<script setup>
import { RelativeTime } from "relative-time-live/vue";
</script>
<template>
<RelativeTime :value="date" style="short" numeric="auto" />
</template>Renders a <time :datetime> element with live text.
i18n
Relative phrases ("2 minutes ago") come entirely from Intl.RelativeTimeFormat, so
any locale the runtime supports works via the locale option. Only the small set of
non-Intl words ("just now", calendar templates, weekday/Today/Yesterday wording
is taken from Intl where possible) are sourced from locale packs.
An English pack ships built-in. Register more:
import { registerLocale } from "relative-time-live";
registerLocale("es", {
justNow: "justo ahora",
calendar: {
sameDay: "hoy a las {time}",
nextDay: "mañana a las {time}",
lastDay: "ayer a las {time}",
nextWeek: "{weekday} a las {time}",
lastWeek: "{weekday} pasado a las {time}",
},
});Lookup is by exact tag then primary subtag (es-MX → es → en).
TypeScript
The package is written in strict TypeScript and ships types for every entry.
import type {
DateInput,
Unit,
RelativeTimeOptions,
CalendarOptions,
RangeOptions,
Ticker,
LocalePack,
} from "relative-time-live";type DateInput = Date | number | string;
type Unit = "second" | "minute" | "hour" | "day" | "week" | "month" | "year";Edge cases
- Invalid dates throw
TypeError. diff === 0(or withinjustNowthreshold) → "just now".- Future vs past is handled by sign;
relativeTimeusesIntl's "in …"/"… ago". numeric: "auto"can yield non-numeric output ("yesterday"); this only happens for ±1 of a unit per Intl rules.- SSR / no DOM: the core and React/Vue hooks never touch
document. OnlyliveTimesrequires a DOM. - Timers: live updates use one shared
setTimeoutchain; subscriptions are recomputed at the soonest needed boundary. Unsubscribing the last subscriber stops the timer.
License
MIT © udayps
