Skip to content

RelativeTime

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/relative-time.json
See it liveSix intervals, four languages, and the difference `numeric` makes.→

The rule this is built on:

A relative time is a convenience, never the only copy of the fact.

“Last week” is quicker to read than a date and useless the moment the reader needs to say when something actually happened — and they cannot get it back, because the page has thrown it away. So the element is a <time> with the exact moment in dateTime and the real date in title: hovering shows it, machines can read it, and nothing is lost by showing the phrase.

<RelativeTime value={row.updated} />
<RelativeTime value={row.updated} numeric="always" /> {/* "1 day ago", not "yesterday" */}

Intl.RelativeTimeFormat writes the phrase, which is the whole reason there is no date library here. “yesterday”, “3 недели назад”, “in 2 months” — the plural rules and the special words for the nearest units are the part a hand-written version gets wrong first, and it gets it wrong only in the languages its author does not read.

What Intl does not decide is which unit to use: it is told “3” and “weeks”. Choosing between them is relativeParts, and it is the only arithmetic here.

It rounds towards zero. Rounding to nearest is the tempting alternative and says “in 1 hour” 31 minutes before the meeting; truncating never claims more time has passed than has.

It does not update itself. A component that re-renders every second to keep “2 minutes ago” honest costs a timer per instance — a table of fifty rows is fifty timers — to correct a number nobody is watching change. A product that needs it re-renders the list on its own schedule, which is what now is for; it also keeps tests off the clock.

A date that is not a date renders as nothing. “Invalid Date” is a string no reader can act on and which looks like a value; an empty <time> with no dateTime says the same to a machine.

The helpers are exported. formatRelative(value, locale, now?, options?) gives the phrase alone (the locale is required: a plain function has no provider to ask), and relativeParts(value, now?) the count and unit behind it.

Props

Read from the types in ui/relative-time.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
valuerequiredstring | number | DateThe moment being described. A Date, epoch milliseconds, or an ISO string - whichever the data already holds.
nownumber | DateWhat counts as now. Given rather than read from the clock so a list can re-render on its own schedule, and so a test is not written against the time it runs at.
localestringThe application's language by default - see useLocale.
numericalways | auto'auto''auto' by default, which is what produces "yesterday" rather than "1 day ago" where the language has a word for it. Pass 'always' for a column where every row should read the same way.
titleOptionsDateTimeFormatOptions{ dateStyle: 'medium', timeStyle: 'short' }How the exact moment is written in the title. The application's own format by default.

And every prop of React's TimeHTMLAttributes<HTMLTimeElement> except title, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 4 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/relative-time.tsx - the file a product copies - newest first.

  1. v0.31.0breaking
    • feat dates and numbers in the application's language 9be5f95
  2. v0.21.0
    • feat a table, its pages, and the two ways a cell is written 32dc405