Scheduling

OpeningHours

A week's opening hours with today marked and whether it is open now, closing soon, or when it opens next.

#schedule#time#status#table#no-js

Demo

Preview

Opening hours

Closed · opens 09:00

Monday (today)09:00–13:0014:00–18:00
Tuesday09:00–18:00
Wednesday09:00–18:00
Thursday09:00–20:00
Friday09:00–18:00
Saturday10:00–02:00
SundayClosedBank holidays too

Closed · opens 14:00

Closes soon · closes 20:00

Open · closes 02:00

Shut · opens tomorrow 09:00

Closed · opens tomorrow 10:00

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

Example

The code of the preview above.

demos/opening_hours.qwe
let opening_hours_demo_week = [    { day: 1, open: "09:00", close: "13:00" }, { day: 1, open: "14:00", close: "18:00" },    { day: 2, open: "09:00", close: "18:00" }, { day: 3, open: "09:00", close: "18:00" },    { day: 4, open: "09:00", close: "20:00" }, { day: 5, open: "09:00", close: "18:00" },    { day: 6, open: "10:00", close: "02:00" },]view OpeningHoursDemo() =    <div.demo-row style="align-items: flex-start; gap: 28px">        <div style="flex: 1 1 280px; max-width: 340px">            <OpeningHours id="opening-hours-demo-now" zone="Europe/Lisbon" hours={opening_hours_demo_week} notes={{ "7": "Bank holidays too" }}/>        </div>        <div.demo-stack style="flex: 1 1 240px">            <OpeningHours id="opening-hours-demo-lunch" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-02T13:20"/>            <OpeningHours id="opening-hours-demo-soon" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-05T19:45"/>            <OpeningHours id="opening-hours-demo-late" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-08T01:15"/>            <OpeningHours id="opening-hours-demo-sunday" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-08T12:00" closed_label="Shut"/>            <OpeningHours id="opening-hours-demo-friday" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-06T19:00"/>        </div>    </div>

Source

ui/opening_hours.qwe
// OpeningHours: a week's opening hours, a row a day, today's row marked, and above them// whether it is open now: "Open · closes 18:00", "Closes soon · 17:45", or "Closed ·// opens Tue 09:00". A day can have more than one span (a lunch break), and a span may// close after midnight. The status is worked out when the page is made, in the place's// own time zone.////   <OpeningHours zone="Europe/Lisbon" hours={[//       { day: 1, open: "09:00", close: "13:00" }, { day: 1, open: "14:00", close: "18:00" },//       { day: 2, open: "09:00", close: "18:00" }, { day: 6, open: "10:00", close: "02:00" },//   ]}/>//// hours: a list of { day (1 Monday to 7 Sunday), open, close ("HH:MM") }; a day with none// is closed. zone: the place's time zone, an IANA name ("Europe/Paris"; "UTC" by default).// at: the moment to judge by instead of now, "YYYY-MM-DDTHH:MM" in the place's own time.// soon: minutes before closing to say "Closes soon" (30). notes: a map of day number to a// line beside it ({ "7": "Bank holidays too" }). closed_label ("Closed"). title: a heading// ("Opening hours"; null for none). compact: just the status line, no table.import ui/theme.qwe use ui_clslet opening_hours_css = css`.ui-opening-hours { display: grid; gap: 10px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-opening-hours-title { margin: 0; font-size: 15px; font-weight: 600; }.ui-opening-hours-status { display: flex; align-items: center; gap: 8px; margin: 0; font-weight: 600; --t: var(--ui-success); }.ui-opening-hours-status::before { content: ""; flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--t); box-shadow: 0 0 0 3px color-mix(in srgb, var(--t) 22%, transparent); }.ui-opening-hours-status span { color: var(--ui-muted); font-weight: 400; }.ui-opening-hours-soon { --t: var(--ui-warning); }.ui-opening-hours-closed { --t: var(--ui-danger); }.ui-opening-hours-table { width: 100%; border-collapse: collapse; }.ui-opening-hours-table th, .ui-opening-hours-table td { padding: 6px 8px; border-top: 1px solid var(--ui-border); text-align: left; vertical-align: top; font-weight: 400; }.ui-opening-hours-table tr:first-child th, .ui-opening-hours-table tr:first-child td { border-top: 0; }.ui-opening-hours-table td { text-align: right; font-variant-numeric: tabular-nums; }.ui-opening-hours-table td span { display: block; }.ui-opening-hours-table small { display: block; color: var(--ui-muted); font-size: 12px; }.ui-opening-hours-off { color: var(--ui-muted); }.ui-opening-hours-today th, .ui-opening-hours-today td { font-weight: 600; background: var(--ui-accent-soft); }.ui-opening-hours-today th { border-radius: var(--ui-radius-sm) 0 0 var(--ui-radius-sm); }.ui-opening-hours-today td { border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0; }`let opening_hours_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]// The minutes since midnight of "HH:MM".fn opening_hours_minutes(t: str) -> int = (int(slice(t, 0, 2)) ?? 0) * 60 + (int(slice(t, 3, 5)) ?? 0)// The spans open in the week as minutes since Monday 00:00: [from, to], a span closing at// or before it opens running into the next day (Sunday's into Monday, round the week).fn opening_hours_spans(hours: list) -> list =    hours | map((h) => {        let base = (h.day - 1) * 1440        let a = opening_hours_minutes(h.open)        let b = opening_hours_minutes(h.close)        { from: base + a, to: base + (b <= a ? b + 1440 : b), day: h.day, open: h.open, close: h.close }    }) | sort_by((s) => s.from)// Open now? { open, soon, until, next } for the minute of the week `w`.fn opening_hours_status(spans: list, w: int, soon: int) -> map {    let week = 7 * 1440    let inside = find(spans, (s) => (w >= s.from && w < s.to) || (s.to > week && w + week >= s.from && w + week < s.to))    if inside != null {        let end = inside.to > week && w < inside.from ? inside.to - week : inside.to        return { open: true, soon: end - w <= soon, until: inside.close, next: null }    }    let ahead = find(spans, (s) => s.from > w) ?? (len(spans) > 0 ? spans[0] : null)    { open: false, soon: false, until: null, next: ahead }}view OpeningHours(hours: list = [], zone: str = "UTC", at: str? = null, soon: int = 30, notes: map = {},                  closed_label: str = "Closed", title: str? = "Opening hours", compact: bool = false, id: str? = null,                  class: str? = null) {    style global opening_hours_css    let local = at ?? slice(date.to_zone(date.now(), zone), 0, 16)    let today = date.weekday(slice(local, 0, 10))    let w = (today - 1) * 1440 + opening_hours_minutes(slice(local, 11, 16))    let spans = opening_hours_spans(hours)    let st = opening_hours_status(spans, w, soon)    let next_text = st.next == null ? "" : (st.next.day == today && st.next.from > w ? `opens ${st.next.open}`        : (st.next.day == today % 7 + 1 ? `opens tomorrow ${st.next.open}` : `opens ${slice(opening_hours_days[st.next.day - 1], 0, 3)} ${st.next.open}`))    <div class={ui_cls(["ui-opening-hours", class])} id={id}>        {title != null && <h3.ui-opening-hours-title>{title}</h3>}        <p class={ui_cls(["ui-opening-hours-status", st.soon && "ui-opening-hours-soon", !st.open && "ui-opening-hours-closed"])} role="status">            {st.open ? (st.soon ? "Closes soon" : "Open") : closed_label}{" "}            <span>{st.open ? `· closes ${st.until}` : (next_text != "" ? `· ${next_text}` : "")}</span>        </p>        {!compact && <table.ui-opening-hours-table>            <tbody>                {for d in range(1, 8) => {                    let mine = hours | filter((h) => h.day == d) | sort_by((h) => h.open)                    let note = get(notes, str(d))                    <tr key={d} class={d == today ? "ui-opening-hours-today" : null} aria-current={d == today ? "date" : null}>                        <th scope="row">{opening_hours_days[d - 1]}{d == today && <span.ui-sr> (today)</span>}</th>                        <td>                            {len(mine) == 0                                ? <span.ui-opening-hours-off>{closed_label}</span>                                : for h in mine => <span key={h.open}>{h.open}–{h.close}</span>}                            {note != null && <small>{note}</small>}                        </td>                    </tr>                }}            </tbody>        </table>}    </div>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.