GrRelativeTime
Overview
When to take it
- freshness matters more than precision — a feed, comments, notifications, an audit log: “5 minutes ago” reads faster than “12.08.2026, 14:32”;
- the label has to stay correct —
liverecomputes the text, and “just now” does not hang there an hour later; - there are many labels on the page — a hundred labels cost one
setInterval: the tick is shared and is removed on a hidden tab; - both forms are needed — the exact date travels into
titleand into the slot, so hovering and a screen reader give the absolute moment.
When to take something else
| Need | Take |
|---|---|
| Choose a date rather than show one | GrDatePicker |
| Choose a moment with a time | GrDateTimePicker |
| Show a sequence of events | GrTimeline |
| Show a countdown to a deadline | a wrapper of your own over useChronoNow |
Exact values are not shown this way. A contract, a payment, a flight schedule require a date:
“in 2 days” does not answer the question “on what date”. cutoff sets the threshold past which the
component moves to the absolute form by itself — and for such places it is set rather than the live
recomputation being switched off.
Examples 3 each with a live demo and its code
The activity of a workspace
Freshness matters more than precision: «2 minutes ago» reads faster than a timestamp, and the exact moment stays in the
titleand the<time datetime>for hover and screen readers.value-adapter="isoDateTime"takes the API strings as they are,numeric="auto"says «yesterday», andbasepins «now» for this frozen page, so the server and the browser agree.Workspace activity - MOMina Ortiz approved invoice INV-0418
- DKDaniel Kim added 3 seats to the Team plan
- BABilling API retried the card of Contoso Ltd
- ATAiko Tanaka changed the dunning schedule
- MOMina Ortiz invited Ben Hartley as an accountant
When invoices are due
A due date is close to a contract: «in 5 days» works, «3 weeks ago» does not answer «which day».
cutoffswitches to the absolute date from 14 days on, formatted byformatoptions rather than a pattern string.value-adapter="isoDate"marks the value as a day, so it is counted in calendar days — at 9 a.m. tomorrow’s date reads «tomorrow», not «in 15 hours». The default slot gets the text, so overdue rows say so in red.Contoso Ltd · open invoicesNet 14 · reminders go out on the due date - INV-0431€1,284.50
- INV-0427€432.00
- INV-0418€3,210.00
- INV-0402€96.00
A label that stays true
The last import is a real moment of this session, so
livekeeps the label true without anyone touching it: «now» becomes «1 minute ago» on a shared timer that ticks every 5 seconds while seconds matter, once an hour for months, and stops on a hidden tab.width="short"fits a status line.Deutsche Bank · business accountTransactions are matched to invoices on import Last import
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
value | TValue | null | undefined | undefined | The moment in question. An empty value renders an empty label. |
valueAdapter |
| undefined | How the value arrives from the consumer. The same contract as in the pickers. |
base | Date | undefined | undefined | What to compare against instead of "now". Set — the clock is not read at all: no timer, no divergence between server and client rendering. That is also what makes tests and snapshots reproducible. |
live | boolean | undefined | undefined | Update live. The component picks the tick itself from the current unit: "3 months ago" need not be recomputed every second. |
cutoff | number | undefined | undefined | From how many days on to show a plain date instead of a relative one. `0` — never: "347 days ago" stays as it is. |
format | Intl.DateTimeFormatOptions | undefined | undefined | The look of the absolute date — through `Intl` options, not a pattern string. |
width | NameWidth | undefined | undefined | Length of the relative string: "3 months ago" versus "3 mo. ago". |
numeric | Intl.RelativeTimeFormatNumeric | undefined | undefined | `'auto'` gives "yesterday", `'always'` — "1 day ago". |
locale | string | undefined | undefined | Display locale. Unset — from the application's i18n adapter. |
Slots
| Slot | Type | Description |
|---|---|---|
default | { text: string; absolute: string; datetime: string; } | Custom markup instead of the text: the same values the component renders itself. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrRelativeTimeConfigurableProps, GrRelativeTimeProps } from '@feugene/granularity-chrono/components/GrRelativeTime'Filedist/types/components/GrRelativeTime/config.d.ts
const grRelativeTimeConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrRelativeTime">;Filedist/types/components/GrRelativeTime/defaults.d.ts
interface GrRelativeTimeConfigurableProps {
width: NameWidth;
numeric: Intl.RelativeTimeFormatNumeric;
cutoff: number;
live: boolean;
}Filedist/types/components/GrRelativeTime/safelist.d.ts
const grRelativeTimeSafelist: string[];Filedist/types/components/GrRelativeTime/GrRelativeTime.vue.d.ts
interface GrRelativeTimeProps<T = Date> {
value?: T | null;
valueAdapter?: GrChronoAdapterName | GrChronoAdapter<T>;
base?: Date;
live?: boolean;
cutoff?: number;
format?: Intl.DateTimeFormatOptions;
width?: NameWidth;
numeric?: Intl.RelativeTimeFormatNumeric;
locale?: string;
}Usage from the library prose
The unit is chosen in two ways, and that is visible at the boundaries
Up to a day the gap is measured by elapsed time — a second is a second, and the calendar has nothing to do with it. From a day onwards it is measured with calendar tuples: days, weeks, full months, years.
Two consequences follow. A month stays a month both in February and in July, because it is counted by the calendar. And a day that lasted 23 hours because of a clock change is called hours — because that much time really did pass; otherwise a two-hour gap across midnight would be called yesterday’s every night.
A date without a time is a day rather than midnight
A value without a time (valueAdapter="isoDate" or an adapter of your own with dateOnly: true)
describes a day in the calendar, and its midnight is a convention of the parsing. Measured by elapsed
time, tomorrow’s date would come out in the morning as “in 15 hours”, and yesterday’s as “9 hours
ago”. Such a value is measured by the calendar alone, from today: “tomorrow”, “today”, “yesterday”,
beyond that weeks and months. The datetime of the <time> receives the date itself (2026-10-07)
rather than a moment in UTC: a local midnight east of Greenwich is the evening of the previous day in
UTC.
"Now" is never in the future
The shared clock is updated by a tick, and between ticks its “now” lags behind the present. That is
why the component rereads the clock when the value changes and when it moves to a ticker with another
tick, rather than comparing with the last tick: otherwise something that has just happened would read
as “in 4 seconds”, and a label that has grown into minutes would see seconds again on the lagging
minute ticker and swing between the tickers. A future closer than the seconds tick (five seconds) is
“now” as well: the source of the value is often a couple of seconds fast. A zero gap is printed as
“now”, and with numeric="always" as “0 sec. ago”, but not as “in 0 sec.”. With base the
comparison is with the given moment, and there is no tolerance: two seconds ahead is the future.
The tick depends on the unit
Seconds are recomputed once every five seconds, months once an hour. One tick for all of them would mean either a second jerking a hundred labels for nothing, or a minute because of which “just now” holds longer than it is true.
Props in this example