Feedback

Countdown

Days, hours, minutes and seconds left until a date-time, ticking in the browser, then an ended message.

#time#clock#interactive

Demo

Preview
52 days, 15 hours, 25 minutes, 6 seconds
1 hour, 30 minutes, 0 seconds
10 minutes, 0 seconds
Early-bird sale has endedEarly-bird sale has ended
A three-second countdown, inside a component.

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/countdown.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/countdown.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/countdown.qwe
component CountdownDemoLive() {    state run = 0    state done = 0    <div.demo-row>        {run > 0            ? <Countdown key={run} seconds={3} variant="inline" label="Launching in" ended="Launched"                         on_end={() => { done += 1 }}/>            : <span.demo-note>A three-second countdown, inside a component.</span>}        <button type="button" @click={() => { run += 1 }}>{run > 0 ? "Again" : "Start"}</button>        {done > 0 && <span.demo-note>on_end called {done} {done == 1 ? "time" : "times"}</span>}    </div>}view CountdownDemo() {    // The next New Year, so the demo never counts to one gone by.    let year = int(slice(date.today(), 0, 4)) + 1    <div.demo-stack>        <Countdown to={`${year}-01-01T00:00:00Z`} label="New Year (UTC) in"/>        <div.demo-row style="gap: 28px">            <Countdown seconds={5400} label="Sale ends in" size="sm"/>            <Countdown seconds={600} variant="inline" label="Your seat is held for" ended="Seat released"/>        </div>        <Countdown to="2026-01-01T09:00:00+01:00" label="Early-bird tickets" ended="Early-bird sale has ended"/>        <CountdownDemoLive/>    </div>}

Source

ui/countdown.qwe
// Countdown: the days, hours, minutes and seconds left until a moment, ticking// each second in the browser, then saying it has ended. The server writes the time// left when it makes the page, so it reads right before any script runs.////   <Countdown to="2026-12-31T23:59:59Z" label="Sale ends in"/>//   <Countdown seconds={600} variant="inline" label="Your seat is held for" ended="Seat released"/>//// to: the moment, an ISO date-time ("2026-12-31T23:59:59Z", "2026-12-31T18:00+02:00",// or a date, "2026-12-31", which is its midnight); without an offset it is UTC.// seconds: instead of to, a number of seconds from when the page is made.// label: what it counts down to, shown above it and read by screen readers (role// "timer"; they are told when it ends). ended: what it says once the time is up// ("Ended"). variant: "boxes" (default), a box per unit, or "inline", "2d 04:12:09"// in the line of text. Days show while there is at least one. size: "sm", "md"// (default) or "lg". on_end: called once, with nothing, when it reaches zero, when// the Countdown is inside one of your components.import ui/theme.qwe use ui_clslet countdown_css = css`.ui-cd { display: inline-grid; gap: 6px; font: 13px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-cd-label { color: var(--ui-muted); }.ui-cd-units { display: flex; gap: 8px; }.ui-cd-unit { display: grid; justify-items: center; gap: 2px; min-width: 58px; padding: 8px 6px 6px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); }.ui-cd-num { font-size: 24px; font-weight: 650; line-height: 1.1; font-variant-numeric: tabular-nums; }.ui-cd-name { color: var(--ui-muted); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }.ui-cd-sm .ui-cd-unit { min-width: 44px; padding: 5px 4px 4px; }.ui-cd-sm .ui-cd-num { font-size: 17px; }.ui-cd-sm .ui-cd-name { font-size: 10px; }.ui-cd-lg .ui-cd-unit { min-width: 76px; padding: 12px 8px 8px; }.ui-cd-lg .ui-cd-num { font-size: 34px; }.ui-cd-inline { display: inline-flex; align-items: baseline; gap: 6px; font-size: inherit; }.ui-cd-clock { font-weight: 600; font-variant-numeric: tabular-nums; }.ui-cd-ended { font-weight: 600; color: var(--ui-muted); }`// n (a whole number, 0 or more) with two digits at least: 7 is "07".fn countdown_two(n: int) -> str = n < 10 ? `0${n}` : str(n)// The milliseconds since 1970 of an ISO date or date-time, or null if it is not one.fn countdown_parse(s: str) -> int? {    let m = re.find(trim(s), "^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2}))?(?:\\.\\d+)?)?\\s*(Z|z|[+-]\\d{2}:?\\d{2})?$")    if m == null { return null }    let g = (i) => int(m.groups[i] ?? "0") ?? 0    let y = g(0)    let mo = g(1)    let yy = mo <= 2 ? y - 1 : y    let era = floor(yy / 400)    let yoe = yy - era * 400    let doy = floor((153 * ((mo + 9) % 12) + 2) / 5) + g(2) - 1    let day = era * 146097 + yoe * 365 + floor(yoe / 4) - floor(yoe / 100) + doy - 719468    let zone = m.groups[6] ?? "Z"    let digits = replace(zone, ":", "")    let offset = upper(zone) == "Z" ? 0        : (starts_with(zone, "-") ? -1 : 1) * ((int(slice(digits, 1, 3)) ?? 0) * 60 + (int(slice(digits, 3, 5)) ?? 0))    ((day * 24 + g(3)) * 60 + g(4) - offset) * 60000 + g(5) * 1000}component Countdown(to: str? = null, seconds: int? = null, label: str? = null, ended: str = "Ended",                    variant: str = "boxes", size: str = "md", on_end = null, class: str? = null) {    style global countdown_css    state target = to != null ? (countdown_parse(to) ?? now()) : now() + (seconds ?? 0) * 1000    state at = now()    let left = target > at ? ceil((target - at) / 1000) : 0    let done = left <= 0    if !done {        every(1000, () => {            at = now()            if at >= target && on_end != null { on_end() }        })    }    let d = floor(left / 86400)    let h = floor(left % 86400 / 3600)    let mi = floor(left % 3600 / 60)    let s = left % 60    let units = (d > 0 ? [[d, "Days", "day"]] : []) + [[h, "Hours", "hour"], [mi, "Minutes", "minute"], [s, "Seconds", "second"]]    let spoken = join(units | filter((u) => u[0] > 0 || u[2] == "second") | map((u) => `${u[0]} ${u[2]}${u[0] == 1 ? "" : "s"}`), ", ")    <div class={ui_cls(["ui-cd", variant == "inline" && "ui-cd-inline", size != "md" && `ui-cd-${size}`, done && "ui-cd-done", class])}         role="timer" aria-label={label}>        {label != null && <span.ui-cd-label aria-hidden="true">{label}</span>}        {done            ? <span.ui-cd-ended>{ended}</span>            : (variant == "inline"                ? <span.ui-cd-clock aria-hidden="true">{d > 0 ? `${d}d ` : ""}{countdown_two(h)}:{countdown_two(mi)}:{countdown_two(s)}</span>                : <span.ui-cd-units aria-hidden="true">                      {for u in units => <span.ui-cd-unit key={u[1]}>                          <span.ui-cd-num>{u[2] == "day" ? str(u[0]) : countdown_two(u[0])}</span>                          <span.ui-cd-name>{u[1]}</span>                      </span>}                  </span>)}        {!done && <span.ui-sr>{spoken}</span>}        <span.ui-sr aria-live="polite">{done ? ended : ""}</span>    </div>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.