Scheduling

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.

#schedule#time#text#interactive

Demo

Preview
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.qwe
main.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>}
Thread
64Useful? Like it, or add to its thread below.

Thread 0 posts

On RelativeTime: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.