Time is one of those things that looks simple until you actually have to render it. A timestamp sitting in your database is a single, unambiguous moment, but the string you show a human depends on where they are, what language they speak, what timezone their laptop is set to, and how long ago the event happened. "April 1, 2014" is fine, but "6 years ago" is friendlier, and "30 seconds ago" needs to quietly become "1 minute ago" while the page is still open. Doing all of this by hand usually means reaching for a date library and writing a little update loop.
The @github/relative-time-element package takes a different route. It is a Web Component, a custom HTML element called <relative-time>, built and maintained by GitHub and used in production across GitHub.com itself. If you have ever seen "opened 3 days ago" or "pushed 2 hours ago" on a pull request, you have already met it. Instead of an imperative JavaScript API, you get a plain HTML tag: the server renders a cacheable fallback string, and the browser upgrades it into a localized, timezone-aware, auto-updating timestamp. It works in React, Vue, Svelte, Rails, plain HTML, or anything else that can emit a tag, because it is just an element.
Why a Custom Element Beats a Helper Function
The clever part of this library is what it does not ship. It has zero runtime dependencies and carries no locale data of its own, because it is built directly on the browser's native Intl.DateTimeFormat and Intl.RelativeTimeFormat APIs. That keeps the bundle tiny while still giving you correct translations for every locale the browser supports.
Because it is an HTML element rather than a function call, it brings a few things along for free:
- Progressive enhancement. The text inside the tag is your server-rendered fallback. If JavaScript is disabled or has not run yet, visitors still see a perfectly readable date.
- Cacheable HTML. Your server can emit one fixed string and cache it aggressively. The browser handles per-user localization on the client, so a single cached response works for everyone.
- Auto-updating. The element schedules its own re-renders, so "1 minute ago" becomes "2 minutes ago" with no code from you.
- Framework agnostic. No bindings, no wrappers, no integration package. A tag is a tag.
It is genuinely maintained, too. The repository sits at around 4,000 stars with a low open-issue count and commits landing as recently as mid-2026, which is what you would expect from something GitHub relies on internally.
Getting It Into Your Project
Install it from npm:
npm install @github/relative-time-element
Or with yarn:
yarn add @github/relative-time-element
Then import it once, anywhere in your app's entry point. The import has a side effect: it registers the <relative-time> custom element with the browser, so you only need to do it a single time.
import '@github/relative-time-element'
The library relies on Intl.RelativeTimeFormat and Intl.DateTimeFormat, which are native in all modern browsers (Chrome, Firefox, Safari 14+, and Edge 79+). Only genuinely ancient browsers would need Intl and custom-element polyfills.
Your First Relative Timestamp
The minimal setup is a tag with a datetime attribute holding an ISO 8601 string, plus some fallback text as its content.
<relative-time datetime="2014-04-01T16:30:00-08:00">
April 1, 2014
</relative-time>
That inner "April 1, 2014" is the cacheable fallback the server emits. Once the element upgrades in the browser, the displayed text changes depending on when the visitor is looking. Recently it might read "30 seconds ago" or "now"; far in the future it reads "6 years from now"; and once enough time passes it flips to an absolute date like "on Apr 1, 2014".
In a React component, you render the tag exactly like any other element. The only wrinkle is TypeScript, which does not know about <relative-time> until you teach it. A small declaration merge fixes that:
import {RelativeTimeElement} from '@github/relative-time-element'
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'relative-time': React.DetailedHTMLProps<
React.HTMLAttributes<RelativeTimeElement>,
RelativeTimeElement
> &
Partial<Omit<RelativeTimeElement, keyof HTMLElement>>
}
}
}
With that in place, your component is plain JSX:
export function CommentMeta({createdAt}: {createdAt: string}) {
return (
<relative-time datetime={createdAt}>
{new Date(createdAt).toLocaleDateString()}
</relative-time>
)
}
Pass attribute values as strings, since that is all HTML attributes can hold, and you are done. Server-side rendering is effortless here, because the output is just a tag with a text child.
Choosing Between Relative and Absolute
The format attribute controls the overall style. The default behavior shows relative text and then switches to an absolute date once the moment is far enough in the past or future. You can also pin it to a specific mode.
<!-- Always a localized absolute date: "Wed, 26 Aug 2021" -->
<relative-time datetime="2021-08-26T00:00:00Z" format="datetime">
Aug 26, 2021
</relative-time>
<!-- Relative text that flips to absolute past a threshold -->
<relative-time datetime="2021-08-26T00:00:00Z" format="relative">
Aug 26, 2021
</relative-time>
There is also a duration format that breaks an interval into its parts, producing strings like "4 hours, 2 minutes, 30 seconds", which is handy for countdowns or elapsed-time displays.
Drawing the Line With Thresholds
The most useful knob for a typical feed or activity list is threshold. It decides the point at which the element stops saying "ages ago" and starts showing a real calendar date. The value is an ISO 8601 duration, and it defaults to P30D, meaning thirty days. You can pair it with a prefix to control the little word in front of absolute dates.
<relative-time
datetime="1970-04-01T16:30:00-08:00"
threshold="P0S"
prefix="this happened on"
>
April 1, 1970
</relative-time>
Setting threshold="P0S" forces the element to always show an absolute date, while a longer threshold keeps text relative for longer. This single attribute lets you express a product decision ("show relative time for the first week, then just show the date") declaratively, with no conditional logic in your code.
Timezones and Language Without the Boilerplate
One of the nicest design touches is DOM inheritance. If a <relative-time> element does not have its own time-zone or lang attribute, it walks up the DOM tree looking for an ancestor that does, falling back to the browser's timezone and 'en' if it finds nothing. That means you can configure a whole subtree once.
<div time-zone="America/New_York" lang="es">
<relative-time datetime="2024-06-01T12:00:00Z">June 1, 2024</relative-time>
<relative-time datetime="2024-07-04T20:00:00Z">July 4, 2024</relative-time>
</div>
Every timestamp inside that wrapper renders in New York time and in Spanish, without repeating attributes on each tag. If you let a user pick their preferred timezone, you set it on one container and the whole page follows.
Clamping the Tense
Occasionally a date should never read as being in the future, even if the clock disagrees because of a small skew or a scheduled event that has technically not happened yet. The tense attribute solves this. Forcing tense="past" makes a future date display as "now" rather than "in the future".
<relative-time datetime="2038-04-01T16:30:00-08:00" tense="past">
April 1, 2038
</relative-time>
This is exactly the right tool for a "last seen" badge or a "last updated" label, where reading "in 3 minutes" would just look like a bug. You can also use precision to limit granularity, so that anything under a minute simply reads "now" when you set precision="minute" alongside format="relative".
Styling the Output
Because the rendered text lives inside the element's internals, you reach it with the CSS ::part() selector rather than ordinary descendant selectors.
relative-time::part(root) {
color: rebeccapurple;
font-weight: bold;
}
This keeps the element encapsulated while still giving you a clean, intentional styling hook.
How It Compares
It is worth knowing where this sits relative to the usual suspects. The native Intl.RelativeTimeFormat is the primitive underneath everything, but it is deliberately low level: you call format(value, unit) and get a string, with no auto-updating, no threshold-to-absolute fallback, no element, and no progressive enhancement. This library is essentially the batteries-included wrapper around that primitive.
Compared to timeago.js, which is famously tiny but ships its own locale dictionaries and is a JavaScript API rather than an HTML element, relative-time-element leans on native Intl for translations and gives you the no-JS fallback for free. And next to dayjs with its relative-time plugin, the difference is scope: Day.js is a full date-manipulation library you would reach for when you also need parsing and arithmetic, whereas this element does one job, formatting a timestamp for display, and does it as markup. If you only need a date to politely say "3 hours ago" and keep itself current, the dedicated element is the lighter, more declarative choice.
Closing Thoughts
There is something satisfying about replacing a tangle of date helpers and refresh timers with a single tag. @github/relative-time-element takes a problem most of us solve badly a dozen times across a codebase and turns it into one declarative element that respects the visitor's locale, honors their timezone, degrades gracefully without JavaScript, and quietly keeps itself up to date. It is small, dependency-free, battle-tested at GitHub's scale, and works the same in any framework because it is built on web standards rather than around them. The next time you find yourself about to import a date library just to print "5 minutes ago", consider that the platform, with a little help from this element, already has you covered.