Relative time

"3 hours ago", kept current, in the reader's own language - and a timestamp with no zone read as UTC instead of as the server's local time.

A timestamp that reads as “3 hours ago” and keeps itself current. The rendering is @github/relative-time-element, which already owns Intl.RelativeTimeFormat per locale and a re-render schedule that slows down as the date ages - enigma add relative-time installs it for you.

What the package adds is the part every hand-rolled wrapper gets wrong.

Customize

The first control moves the date rather than a prop, because how old it is decides whether it reads as a phrase or a date. Set a locale and it speaks that language.

Usage

import { RelativeTime } from "@enigmax/primitives/react";
import "@enigmax/primitives/relative-time.css";

<RelativeTime date={comment.createdAt} />
// -> 3 hours ago

<RelativeTime date="2026-08-13 22:41:00" numericBeyondThreshold />
// -> 13/8/2026 once it is older than the threshold

Installation

Adds the package and prints the import to use. Append --copy to write the source into your project instead.

enigma add relative-time

Brings @github/relative-time-element with it. --no-deps leaves them out.

Installs with your project's own package manager - npm, pnpm, yarn or bun - read from its packageManager field or its lockfile.

A timestamp with no zone is UTC

The single most common defect in a date column. 2026-08-13 22:41:00 comes back with no offset, new Date() reads it as local time, and every reader east or west of the server sees a time that is hours out - silently, because a wrong time is still a valid one.

<RelativeTime date="2026-08-13 22:41:00" />   // read as UTC, as the database meant it
<RelativeTime date="2026-08-13" />            // left alone: a date with no clock is already UTC

A date with no clock is deliberately untouched. Appending a zone to it produces 2026-08-13Z, which is outside the spec’s grammar and falls to each engine’s own legacy parser - a portability coin flip on a value that was already correct.

It works before JavaScript does

The absolute date is rendered as the element’s child, so a server render, a page whose JavaScript has not arrived, and a reader with scripting off all see a real date. Once the element upgrades it renders the relative phrase into a shadow root and the child stops being shown - so the fallback costs nothing and is never seen twice.

<!-- what the server sends -->
<relative-time datetime="2026-08-13T22:41:00.000Z">on Aug 13, 2026</relative-time>

Pass now to pin the instant the age is measured from, and a server render and its hydration agree exactly. Where you cannot, the component sets suppressHydrationWarning - a timestamp is the one hydration difference React has an escape hatch for rather than a bug.

Props

Default
date ISO string, epoch ms, or a Date
format "auto" Relative until the threshold, then a date
threshold "P30D" ISO 8601 duration. Past this, auto stops counting
prefix "on" The word before an absolute date. "" removes it
locale Undefined lets the element read the page’s own lang
numericBeyondThreshold false Past the threshold, render digits instead of a month name
capitalizeFirst true So a standalone “yesterday” starts a line properly
fallback null Rendered when the date cannot be parsed

numericBeyondThreshold reads the threshold rather than approximating it. The version this came from used a fixed three-to-ninety-day window, which disagrees with threshold the moment anyone sets one.