RelativeTime
"3 minutes ago" or "in 2 hours" from an ISO time, kept up to date in the browser, with the full date on hover.
Demo
Saved
- Posted
- Edited
- Joined
- Starts
- Short:
,
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/relative_time.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/relative_time.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/relative_time.qwe
component RelativeTimeDemoLive() { state stamp = date.from_ms(now() - 12000, "UTC") <div.demo-row id="relative-time-demo-live"> <span>Saved <RelativeTime time={stamp}/></span> <button type="button" @click={() => { stamp = date.from_ms(now() - 12000, "UTC") }}>Save again</button> <button type="button" @click={() => { stamp = date.from_ms(now() - 3 * 3600000, "UTC") }}>Saved 3 hours ago</button> </div>}view RelativeTimeDemo() = <div.demo-stack> <RelativeTimeDemoLive/> <ul.demo-stack id="relative-time-demo-fixed" style="margin: 0; padding-left: 18px; gap: 4px; font-size: 14px"> <li>Posted <RelativeTime time="2026-10-28T13:55:00Z" at="2026-10-28T14:00:00Z"/></li> <li>Edited <RelativeTime time="2026-10-27T09:00:00Z" at="2026-10-28T14:00:00Z"/></li> <li>Joined <RelativeTime time="2026-08-01" at="2026-10-28T14:00:00Z"/></li> <li>Starts <RelativeTime time="2026-10-28T16:30:00Z" at="2026-10-28T14:00:00Z"/></li> <li>Short: <RelativeTime time="2026-10-21T14:00:00Z" at="2026-10-28T14:00:00Z" style="short"/>, <RelativeTime time="2026-10-28T13:59:57Z" at="2026-10-28T14:00:00Z" style="short" title={false}/></li> </ul> </div>Source
ui/relative_time.qwe
// RelativeTime: how long ago (or how soon) a moment is, "3 minutes ago", "in 2 hours",// "yesterday", kept up to date in the browser: every few seconds while it is under a// minute, every half minute under an hour, less often after. It is a <time> with the// moment for machines and the full date in its title, shown on hover.//// <RelativeTime time="2026-10-28T09:12:00Z"/> 3 hours ago// <RelativeTime time={post.created} style="short"/> 3h// <RelativeTime time="2026-12-24T18:00:00Z" at="2026-12-24T16:00:00Z"/> in 2 hours//// time: the moment, an ISO date-time with its offset ("2026-10-28T09:12:00Z") or a date// ("2026-10-28", its midnight in UTC). at: a fixed moment to count from instead of the// clock, which then does not tick (for tests, or a page made for a given time).// style: "long" (default, "3 minutes ago") or "short" ("3m", "2h", "5d", "now").// Under 10 seconds is "just now"; past a week it counts weeks, then months, then years.// title: false leaves out the full date on hover.import ui/theme.qwe use ui_clslet relative_time_css = css`.ui-relative-time { color: inherit; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-relative-time[title] { text-decoration: underline dotted color-mix(in srgb, currentColor 35%, transparent); text-underline-offset: 3px; cursor: help; }`// How far apart two moments are, in words: ms is now minus the moment (above 0 is past).fn relative_time_text(ms: num, style: str) -> str { let past = ms >= 0 let s = floor(abs(ms) / 1000) let short = style == "short" let say = (n: int, word: str, letter: str) => { let text = short ? `${n}${letter}` : `${n} ${word}${n == 1 ? "" : "s"}` short ? text : (past ? `${text} ago` : `in ${text}`) } if s < 10 { return short ? "now" : "just now" } if s < 60 { return say(s, "second", "s") } let m = floor(s / 60) if m < 60 { return say(m, "minute", "m") } let h = floor(m / 60) if h < 24 { return say(h, "hour", "h") } let d = floor(h / 24) if d == 1 && !short { return past ? "yesterday" : "tomorrow" } if d < 7 { return say(d, "day", "d") } if d < 30 { return say(floor(d / 7), "week", "w") } if d < 365 { return say(floor(d / 30), "month", "mo") } say(floor(d / 365), "year", "y")}component RelativeTime(time: str, at: str? = null, style: str = "long", title: bool = true, class: str? = null) { style global relative_time_css state tick = now() let moment = date.ms(time) let fixed = at != null ? date.ms(at) : null let gap = (fixed ?? tick) - moment let wait_ms = abs(gap) < 60000 ? 5000 : (abs(gap) < 3600000 ? 30000 : 300000) // A new time is counted from the clock as it is now, not as it was at the last tick. // The first one keeps the server's clock, so the browser takes up the server's text. state counted = moment if fixed == null && moment != counted { counted = moment tick = now() } if fixed == null { every(wait_ms, () => { tick = now() }) } let full = len(time) > 10 ? date.format(time, "dddd D MMMM YYYY, HH:mm") : date.format(time, "dddd D MMMM YYYY") <time class={ui_cls(["ui-relative-time", class])} datetime={time} title={title ? full : null} aria-label={title ? `${relative_time_text(gap, "long")} (${full})` : null}>{relative_time_text(gap, style)}</time>}Related
AgendaListEvents grouped by day with the date at the left, each with its time, title and place; today and tomorrow named.6 views · 6 downloadsDayViewOne day as a column of hours with its events drawn at their times, side by side when they overlap, and all-day events above.7 views · 7 downloadsEventCardAn event as a card: a date block, title, time, place, a line about it, an optional picture and badge, and a button.7 views · 6 downloadsOpeningHoursA week's opening hours with today marked and whether it is open now, closing soon, or when it opens next.6 views · 6 downloadsStopwatchA stopwatch to the hundredth: Start, Lap, Stop and Reset, with the laps listed and the fastest and slowest marked.7 views · 5 downloadsTimerA countdown set in minutes with Start, Pause and Reset, a ring that empties, presets, and a silent alert at zero.7 views · 7 downloads
Thread
Thread 0 posts
On RelativeTime: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.