relative-time-element
Localized, auto-updating relative timestamps as a drop-in <time> element replacement, no framework required.
Repository Health
Technical Analysis
relative-time-element is a small, framework-agnostic web component that extends the standard HTML <time> element to display timestamps as localized, human-readable relative text (“20 minutes ago”, “in 3 days”) that keeps itself updated in the browser without any polling code from the host page. Because the server only ever needs to emit a plain ISO 8601 datetime attribute and a static fallback text, HTML fragments stay cacheable while every visitor still sees a correctly localized, timezone-aware time once JavaScript takes over.
Under the hood it wraps the standard Intl.DateTimeFormat and Intl.RelativeTimeFormat APIs (with a small ponyfill for the newer Intl.DurationFormat proposal) and schedules re-renders through a single shared timer that throttles its own cadence as elapsed time grows, so a page with hundreds of <relative-time> elements does not spin up hundreds of independent intervals. Locale, time zone, and hour-cycle can all be inherited from ancestor elements or the document, letting one page mix per-user or per-widget formatting rules.
What You Get
- A
<relative-time>custom element you drop straight into server-rendered HTML, with the cached fallback text shown until JavaScript upgrades it - Three output formats —
relative(“3 hours ago”),datetime(a localized absolute date), andduration(a countdown/count-up breakdown) — switchable via a singleformatattribute - Automatic, self-throttling re-render scheduling shared across every instance on the page via one internal timer, instead of a per-element interval
- Locale, time-zone, and hour-cycle resolution that falls back from the element’s own attributes to the nearest ancestor to the document, so one page can mix formatting rules
- A
relative-time-updatedevent fired on every re-render so host code can react to text/title changes without polling
Common Use Cases
- Rendering “posted 2 hours ago” / “updated just now” timestamps on cached server-rendered pages (comments, activity feeds, commit lists) without invalidating the HTML cache per viewer
- Showing countdowns or elapsed durations for deadlines, expirations, or running processes via
format="duration" - Displaying fully localized absolute dates (
format="datetime") that still respect the visitor’s browser locale and timezone preferences - Adding accessible, auto-updating timestamps to any static site or non-React/Vue stack, since it’s a plain custom element with no framework dependency
Under The Hood
Architecture
The library is a single custom element class (RelativeTimeElement in src/relative-time-element.ts) that extends HTMLElement and drives its behavior entirely through observedAttributes/attributeChangedCallback lifecycle hooks rather than a virtual-DOM diffing layer. Rendering, duration math, and locale-aware formatting are deliberately split into separate modules: duration.ts implements a Temporal-proposal-style Duration value type (parsing/comparing/applying ISO 8601 durations), intl-cache.ts owns a bounded LRU cache of Intl.DateTimeFormat/Intl.RelativeTimeFormat/Intl.NumberFormat/Intl.Locale instances keyed by locale+options, and relative-time-element-define.ts handles idempotent customElements.define registration. Re-render scheduling is centralized in a single module-level dateObserver object that every live element registers with; it computes the soonest next-update time across all observed elements and drives them from one shared setTimeout, so per-instance elapsed-time granularity determines cadence (seconds while recent, hours once stale) without one timer per element. If the shared Duration/intl-cache modules changed shape, every consumer of relative/duration/datetime formatting would break simultaneously, since all three formats route through them.
Tech Stack
Written in TypeScript (strict tsconfig.json) targeting ES modules, compiled with tsc and bundled to a single ESM file with esbuild --bundle --keep-names. It has zero runtime dependencies — it only leans on browser-native Intl.DateTimeFormat/Intl.RelativeTimeFormat, with a hand-written ponyfill (duration-format-ponyfill.ts) standing in for the not-yet-universal Intl.DurationFormat. Custom-elements documentation is auto-generated via @custom-elements-manifest/analyzer into custom-elements.json, and linting runs through GitHub’s own shared ESLint configs (eslint-plugin-github, eslint-plugin-custom-elements) plus tsc --noEmit.
Code Quality
Test coverage is unusually deep for the library’s size: the test/ directory (~4,000 lines) is roughly 3x the size of src/ (~1,300 lines), covering the element’s attribute/format matrix, the Duration parser/comparator, the Intl.DurationFormat ponyfill, and the LRU intl-cache eviction behavior, run via @web/test-runner against real Playwright-driven Chromium rather than a DOM shim. Errors are handled defensively at formatting boundaries (e.g. isBrowser12hCycle and locale resolution both wrap Intl calls in try/catch and fall back to safe defaults rather than throwing into the DOM lifecycle). Naming is consistent and privacy is enforced with real private class fields (#renderRoot, #updating), and CI runs the full test suite plus lint/typecheck on every push and PR via GitHub Actions.
API Design
The element’s public surface is entirely HTML attributes and DOM properties mirroring native <time>-like conventions (datetime, plus opt-in format, tense, precision, threshold, and formatting attributes that map 1:1 onto Intl.DateTimeFormat option names), so anyone who knows Intl.DateTimeFormat already knows most of this API. Getting started requires zero JavaScript — one <script type="module"> import and a <relative-time datetime="..."> tag — with framework-specific behavior (locale/timezone inheritance from ancestors, a custom relative-time-updated event) layered on as progressive enhancement rather than required ceremony.