GrRelativeTime
Machine-translated from the Russian original, not yet reviewed. Read the original
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.
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.
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.
Install
npm i @feugene/granularity-chronoImport
import { GrRelativeTime } from '@feugene/granularity-chrono/components/GrRelativeTime'API
The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.
Examples 3
Cutoff
<script setup lang="ts">
// `GrRelativeTime` подставляется авто-импортом (`unplugin-vue-components`).
const now = new Date(2026, 7, 12, 12, 0)
const posts = [
new Date(2026, 7, 12, 9, 30),
new Date(2026, 7, 5, 12, 0),
new Date(2026, 6, 2, 12, 0),
new Date(2025, 10, 20, 12, 0),
]
/** Вид абсолютной даты — опциями `Intl`, а не строкой-паттерном. */
const format: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }
</script>
<template>
<div class="grid gap-2">
<p class="showcase-demo-text text-sm opacity-70">
Старше 30 дней — обычная дата: «347 дней назад» не помогает никому.
</p>
<div v-for="post in posts" :key="post.toISOString()" class="text-sm">
<GrRelativeTime :value="post" :base="now" :cutoff="30" :format="format" />
</div>
</div>
</template>Live
<script setup lang="ts">
import { ref } from 'vue'
// `GrRelativeTime` подставляется авто-импортом (`unplugin-vue-components`).
import { GrButton } from '@feugene/granularity'
/**
* Здесь `base` не задан: отсчёт идёт от общих часов пакета, и текст обновляется
* сам. Такт компонент выбирает по единице — секунды пересчитываются часто,
* месяцы редко, а таймер на такт в приложении один на всех.
*/
const events = ref<Date[]>([new Date()])
function add(): void {
events.value = [new Date(), ...events.value].slice(0, 5)
}
</script>
<template>
<div class="grid gap-3 justify-items-start">
<GrButton size="sm" @click="add">
Отметить событие
</GrButton>
<ul class="grid gap-1 text-sm">
<li v-for="event in events" :key="event.toISOString()">
<GrRelativeTime :value="event" />
</li>
</ul>
</div>
</template>Scale
<script setup lang="ts">
// `GrRelativeTime` подставляется авто-импортом (`unplugin-vue-components`).
/**
* Момент отсчёта задан пропом `base` — от него и считается вся шкала. Так
* пример показывает одно и то же в любой день и не зависит от часов машины,
* на которой открыт.
*/
const now = new Date(2026, 7, 12, 12, 0)
function ago(ms: number): Date {
return new Date(now.getTime() - ms)
}
const scale = [
{ label: 'секунды', value: ago(3 * 1000) },
{ label: 'минуты', value: ago(3 * 60_000) },
{ label: 'часы', value: ago(4 * 3_600_000) },
{ label: 'вчера', value: new Date(2026, 7, 11, 12, 0) },
{ label: 'недели', value: new Date(2026, 6, 29, 12, 0) },
{ label: 'месяцы', value: new Date(2026, 4, 12, 12, 0) },
{ label: 'годы', value: new Date(2024, 7, 12, 12, 0) },
{ label: 'будущее', value: new Date(2026, 7, 14, 12, 0) },
]
</script>
<template>
<div class="grid gap-2">
<div v-for="row in scale" :key="row.label" class="flex items-baseline gap-3 text-sm">
<span class="showcase-demo-text w-24 shrink-0 opacity-70">{{ row.label }}</span>
<GrRelativeTime :value="row.value" :base="now" />
<GrRelativeTime :value="row.value" :base="now" width="short" class="opacity-70" />
</div>
</div>
</template>