RelativeTime
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/relative-time.jsonThe 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.
| Prop | Type | Default | |
|---|---|---|---|
valuerequired | string | number | Date | The moment being described. A Date, epoch milliseconds, or an ISO string - whichever the data already holds. | |
now | number | Date | What 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. | |
locale | string | The application's language by default - see useLocale. | |
numeric | always | 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. |
titleOptions | DateTimeFormatOptions | { 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.