Relative Time
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add utility/relative-timePlain-CSS theme — no build step
npx astrocraft-ui add utility/relative-time --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add utility/relative-time --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add utility/relative-time --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add utility/relative-time --theme tailwind --bridge lumosLive demo
Countdown & RelativeTime
Neither one announces on every tick. The countdown's digits are role="timer", whose aria-live is off by definition — readable on demand, silent while running — and a separate polite region speaks only at thresholds. Both read the clock rather than counting: a setInterval is throttled in a background tab and stops while a laptop sleeps.
- Last edited
- Created
View source on those two: the server rendered an absolute date in UTC, and the script replaced it with the relative phrasing in your locale. A relative string baked at build time would still say "3 hours ago" in a fortnight.
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `RelativeTime.astro` | `relative-time` | — | — | — |
Source
What the command copies — 3 files, alias-free so the copy lands standing alone. This is the whole component, shown without its comments; the files you install keep them, with the reasoning behind every rule.
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"time"> & {
datetime: string;
locale?: string;
};
const { datetime, locale, class: className, ...rest } = Astro.props;
const date = new Date(datetime);
const iso = Number.isNaN(date.valueOf()) ? datetime : date.toISOString();
const absolute = Number.isNaN(date.valueOf())
? datetime
: new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
timeZoneName: "short",
}).format(date);
---
<time datetime={iso} title={absolute} class={className} data-slot="relative-time" {...rest}>
{absolute}
</time>
<script>
import { onReadyOnce } from "../../_once";
import { refreshDelay, relativeParts } from "./format";
function wire(el: HTMLElement) {
const ms = Date.parse(el.getAttribute("datetime") ?? "");
if (Number.isNaN(ms)) {
console.error(`RelativeTime: \`datetime\` is not a parseable date: ${el.textContent}`);
return;
}
const lang = document.documentElement.lang || undefined;
const relative = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
const absolute = new Intl.DateTimeFormat(lang, { dateStyle: "long", timeStyle: "short" });
el.title = absolute.format(ms);
const render = () => {
if (!el.isConnected) return;
const diff = ms - Date.now();
const { value, unit } = relativeParts(diff);
el.textContent = relative.format(value, unit);
setTimeout(render, refreshDelay(diff));
};
render();
}
onReadyOnce('[data-slot="relative-time"]', wire);
</script>
export interface RelativeParts {
value: number;
unit: Intl.RelativeTimeFormatUnit;
}
const UNITS: readonly { unit: Intl.RelativeTimeFormatUnit; ms: number }[] = [
{ unit: "year", ms: 31_536_000_000 },
{ unit: "month", ms: 2_592_000_000 },
{ unit: "week", ms: 604_800_000 },
{ unit: "day", ms: 86_400_000 },
{ unit: "hour", ms: 3_600_000 },
{ unit: "minute", ms: 60_000 },
{ unit: "second", ms: 1_000 },
];
export function relativeParts(diffMs: number): RelativeParts {
const diff = Number.isFinite(diffMs) ? diffMs : 0;
const abs = Math.abs(diff);
const bucket = UNITS.find((u) => abs >= u.ms);
if (!bucket) return { value: 0, unit: "second" };
return { value: Math.trunc(diff / bucket.ms), unit: bucket.unit };
}
export function refreshDelay(diffMs: number): number {
const abs = Math.abs(Number.isFinite(diffMs) ? diffMs : 0);
if (abs < 60_000) return 10_000;
if (abs < 3_600_000) return 60_000;
if (abs < 86_400_000) return 900_000;
return 3_600_000;
}
import RelativeTime from "./RelativeTime.astro";
export { refreshDelay, type RelativeParts, relativeParts } from "./format";
export { RelativeTime };
export default RelativeTime;
What you get
The component source, copied into your project by npx astrocraft-ui add utility/relative-time — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.