# relative-time-element: localizing a cached timestamp in the browser instead of on the server

> A custom element that turns a server-rendered ISO 8601 timestamp into a localized or relative phrase using the browser's own Intl APIs, so cached HTML fragments stay correct for every visitor.

**github/relative-time-element** — Web component extensions to the standard <time> element.

- Repository: https://github.com/github/relative-time-element
- Website: https://github.github.io/relative-time-element/examples/
- Stars: 4,038 · Forks: 194
- Language: JavaScript
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/github-relative-time-element

## Why the browser has to do the formatting

The problem this element solves starts with a cache. A server renders a fragment containing a date, the fragment goes into a CDN or reverse proxy, and every subsequent visitor receives that same markup. If the date was formatted on the server, it is formatted in one timezone, in one locale, for one reader, and it is wrong for everyone else.

The README's example makes the mechanism concrete. The server produces this markup and caches it:

```html
<relative-time datetime="2014-04-01T16:30:00-08:00"> April 1, 2014 4:30pm </relative-time>
```

Every visitor gets those identical bytes from the cache. Then the custom element runs in the browser and rewrites the text content using the reader's timezone and locale, which in the README's example becomes a day-first, 24-hour rendering:

```html
<relative-time datetime="2014-04-01T16:30:00-08:00"> 1 Apr 2014 21:30 </relative-time>
```

Note the second detail, because it is the part that makes the pattern safe to deploy: the `datetime` attribute is unchanged. The correct absolute instant is already in the markup, and only the presentation is rewritten. Dates before months and a 24-hour clock both come from the browser's own settings rather than from anything the element decides.

The third detail is the fallback. The README states that if JavaScript is disabled, the default text served in the cached markup is still displayed. That is the whole reason the text content is required in the first place.

## Installing the package and the Intl requirement

It is an ordinary npm package, published under a scoped name:

```bash
npm install @github/relative-time-element
```

The README then names the real constraint, which is more useful than the install line. The element uses `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`, both of which it describes as supported by all modern JavaScript engines. If you need to support an older browser, you may need to introduce a polyfill for those two APIs.

That dependency is the trade-off in the whole design. The element gets timezone handling, locale aware date ordering and relative phrasing from the platform rather than shipping a locale database, which is why the package can be small and why it stays correct as locales change. The price is that it is not usable as-is anywhere `Intl` is missing.

The source is TypeScript and the build output is bundled with esbuild. The manifest's export map is more interesting than most: besides the default entry and a `define` entry, it publishes a `duration` entry and a `relative-time` entry, plus a `relative-time/define` entry that appears to register the element without pulling in the whole bundle. A consumer that only wants the duration formatter does not have to load the element.

## Attributes that decide relative or absolute

The README's attribute table is the reference for this element, and a few entries do most of the work. `format` takes `'datetime'`, `'relative'` or `'duration'` and defaults to `'auto'`, which is what chooses the relative phrasing near the present and an absolute date further away. `tense` takes `'auto'`, `'past'` or `'future'` and also defaults to auto. `precision` takes `'year'`, `'month'`, `'day'`, `'hour'`, `'minute'` or `'second'` and defaults to `'second'`.

`threshold` is the one worth understanding properly. It defaults to `'P30D'`, an ISO 8601 duration of thirty days, and the README explains the consequence in one line: a relative phrase is used for up to a month, and after that the actual date is shown. That single setting is what stops a comment thread from reading "20 days from now" about something from 2014.

`datetime` must be a valid ISO 8601 date and time. There is also a `date` property that takes a `Date` object where `datetime` takes a string, and setting one overrides the other. The README illustrates the `tense` attribute with a deliberately absurd value, a 2038 date that displays as `now` for decades, which tells you the element trusts its input completely and never sanity-checks it.

`formatStyle` takes `'long'`, `'short'` or `'narrow'`, and the table's footnotes describe how it defaults differently depending on the format, including a mention of `elapsed` and `micro` formats in the footnote text. `prefix` defaults to `'on'`, which is the word you see in an absolute rendering, and `timeZone` lets you override the browser default. `noTitle` suppresses the tooltip.

## The property set is an Intl formatter in disguise

Scanning further down the table, most of what remains maps one to one onto an `Intl.DateTimeFormat` option. There are `second`, `minute`, `hour`, `weekday`, `day`, `month` and `year`, each with `'numeric'`, `'2-digit'` or text values where applicable, plus `timeZoneName` with values including `shortOffset` and `longOffset`. `hourCycle` takes `'h11'`, `'h12'`, `'h23'` or `'h24'` and defaults to whatever the browser prefers.

That is the honest framing for this library: it is a custom element shell around the platform's internationalisation primitives, with the caching problem solved and a tidy attribute surface. It is not a date library that reimplements locale data, and it does not compete with one. If you are already formatting dates through `Intl` directly, this element adds the progressive enhancement and the no-JavaScript fallback rather than new formatting capability.

The default resolution rules are also worth knowing because they explain surprising output. `year` returns `'numeric'` when the timestamp falls in the current year and undefined when it falls in a different year, which is why old posts often show a bare date without a year. `weekday` and `month` inherit from `formatStyle` when the format is `'datetime'` and fall back otherwise.

## Recent releases are about correctness and render cost

The release history is short and specific, which tells you the element is in steady maintenance rather than rapid churn. Version 5.2.0, published 2026-06-09, added micro tense phrasing, contributed by an outside author and re-landed after an earlier attempt. Version 5.3.0 on 2026-07-16 is mostly performance: caching `Intl` formatters instead of constructing them on every render, and skipping redundant render-root and title writes on no-op ticks. Both of those matter for a page with a thousand timestamps on it, since a ticking clock that rebuilds formatters sixty times a minute is a real cost.

Version 5.3.1, published 2026-08-03, fixed year precision for the duration and elapsed formats, which is a correctness bug in the same family as the default `year` behaviour described above. The dependency bumps in the same releases, js-yaml and esbuild, arrive through dependabot.

The repository layout backs the tooling claim. There is a `test/` directory and a `web-test-runner.config.js`, the dev dependencies include `@open-wc/testing` and `@web/test-runner-playwright`, so the tests run in real browsers rather than a DOM shim. There is also `tsconfig.json`, an `.eslintrc.json`, a `custom-elements-manifest.config.js` and a `CODEOWNERS` file. The package publishes a `custom-elements.json` manifest, generated by `custom-elements-manifest analyze`, which is how the element shows up in documentation tooling and IDE autocomplete.

The topics are `custom-elements`, `localization`, `timezone`, `web-components` and `keep`, and the last one is the important signal: it is a keep-a-changelog project, so the changelog format follows that convention.

## Conclusion

This element exists to solve one caching problem properly: a fragment rendered once on a server cannot be localized per reader, so the localization has to happen client side after delivery. It does that with the browser's own `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat`, keeps a readable fallback in the element's text content, and exposes enough attributes to control tense, precision and the month threshold. The cost is a dependency on `Intl` support, which the README flags as needing a polyfill for older browsers. Install `@github/relative-time-element`, keep a plain formatted date as the child text, and set `format`, `tense` and `threshold` deliberately rather than relying on the auto defaults.

## FAQ

### What is relative time?

It is phrasing that expresses a timestamp as a distance from now, such as "30 seconds ago" or "an hour from now", rather than as a fixed calendar date. The README lists the phrases this element can produce, from "now" through to "6 years from now", and explains that a relative phrase is used for up to a month before the actual date is shown.

### What is a time element?

The standard HTML `<time>` element holds a machine-readable date or time in a `datetime` attribute. This project is a set of extensions to it, packaged as the `<relative-time>` custom element, which adds localization and auto-updating relative phrasing in the browser.

### How do I use the "time" element in HTML code?

Add a `<relative-time>` element with a valid ISO 8601 value in its `datetime` attribute and put a plain formatted date as its text content, for example `<relative-time datetime="2014-04-01T16:30:00-08:00"> April 1, 2014 </relative-time>`. The text content is the fallback shown when JavaScript is unavailable, and the element rewrites it once it upgrades.

### Can you give me an example of a relative date?

The README lists the outputs the element produces relative to now: "now", "30 seconds ago", "a minute ago", "30 minutes ago", "an hour ago", "20 hours ago", "a day ago", "20 days ago", and forward equivalents such as "7 minutes from now" and "6 years from now". Beyond the threshold, which defaults to thirty days, it shows an absolute date instead.

## Sources

- [github/relative-time-element on GitHub](https://github.com/github/relative-time-element)
- [License: MIT](https://github.com/github/relative-time-element/blob/main/LICENSE)
- [Project website](https://github.github.io/relative-time-element/examples/)
- [README](https://github.com/github/relative-time-element/blob/main/README.md)
- [Releases](https://github.com/github/relative-time-element/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/github-relative-time-element
