DatePicker
A date typed as YYYY-MM-DD or picked in a calendar grid, with the keyboard, limits and days that cannot be chosen.
Demo
November 2026
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
Type it, or press ArrowDown for the calendar.
November 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 |
No date yet.
December 2026
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
December or January; closed on Sundays and over Christmas.
October 2026
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
That is before the order date.
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/date_picker.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/date_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/date_picker.qwe
component DatePickerDemoLive() { state d = null <div.demo-stack> <DatePicker name="dp-demo-birthday" label="Birthday" week_start={0} on_change={(v) => { d = v }}/> <p.demo-note>{d == null ? "No date yet." : `The form sends ${d}.`}</p> </div>}view DatePickerDemo() = <div.demo-stack> <div.demo-row> <DatePicker name="dp-demo-due" label="Due date" value="2026-11-02" hint="Type it, or press ArrowDown for the calendar."/> <DatePickerDemoLive/> </div> <DatePicker name="dp-demo-checkin" label="Check-in" value="2026-12-22" min="2026-12-01" max="2027-01-31" disabled_dates={["2026-12-25", "2026-12-26"]} disabled_weekdays={[0]} required={true} hint="December or January; closed on Sundays and over Christmas."/> <DatePicker name="dp-demo-late" label="Shipped on" value="2026-10-01" size="sm" error="That is before the order date."/> </div>Source
ui/date_picker.qwe
// DatePicker: a date typed as YYYY-MM-DD, or picked in a calendar that opens// beside it. The calendar is the ARIA date-picker dialog: a month as a grid, where// the arrow keys move by day and week, PageUp / PageDown by month (with Shift, by// year), Home / End to the week's start and end, Enter or Space picks, and Escape// closes. Today is marked; dates before `min`, after `max` or in `disabled_dates`// cannot be picked. The text box is a real input named `name`, so a plain <form>// sends "2026-03-14" (it also checks the form without a script, by `pattern`).//// <DatePicker name="due" label="Due date" value="2026-11-02"/>// <DatePicker name="checkin" label="Check-in" min="2026-11-01" max="2027-01-31"// disabled_dates={["2026-12-25", "2026-12-26"]} disabled_weekdays={[0]} required={true}/>// <DatePicker label="Birthday" week_start={0} on_change={(d) => { birthday = d }}/>//// value: "YYYY-MM-DD" to start with. min, max: the first and last dates that can be// chosen. disabled_dates: dates that cannot ("YYYY-MM-DD" each). disabled_weekdays:// days of the week that cannot (0 Sunday ... 6 Saturday). week_start: the calendar's// first column (1, Monday; 0 for Sunday). today: the date to mark as today (the// browser's own date by default). placeholder ("YYYY-MM-DD").// name, label, hint, error, required, disabled, field (the text box's id,// "date-picker-" + name by default) and size ("sm" or "md") as for every field.// A date typed that is not one, or cannot be chosen, is said so under the box, and// the form is not sent until it is put right.// on_change(date): inside one of your components, given "YYYY-MM-DD" (null when// it is emptied).import ui/theme.qwe use ui_clslet date_picker_css = css`.ui-date-picker { position: relative; display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-date-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-date-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-date-picker-box { display: flex; align-items: center; width: 200px; max-width: 100%; height: 38px; padding: 0 4px 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); transition: border-color .15s, box-shadow .15s;}.ui-date-picker-box:hover { border-color: var(--ui-strong); }.ui-date-picker-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-date-picker-invalid .ui-date-picker-box { border-color: var(--ui-danger); }.ui-date-picker-box input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent; color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums;}.ui-date-picker-box input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-date-picker-btn { display: inline-grid; place-items: center; flex: none; width: 30px; height: 30px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-date-picker-btn svg { width: 18px; height: 18px; }.ui-date-picker-btn:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--ui-subtle); color: var(--ui-fg); }.ui-date-picker-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-date-picker-btn:disabled, .ui-date-picker-btn[aria-disabled="true"] { opacity: .4; cursor: not-allowed; }.ui-date-picker-pop { position: absolute; z-index: 30; top: 100%; left: 0; width: 300px; max-width: calc(100vw - 32px); margin-top: 4px; padding: 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow-lg);}.ui-date-picker-pop[hidden] { display: none; }.ui-date-picker-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }.ui-date-picker-head h2 { margin: 0; font-size: 14px; font-weight: 600; }.ui-date-picker-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }.ui-date-picker-grid th { padding: 4px 0; font-size: 12px; font-weight: 500; color: var(--ui-muted); text-align: center; }.ui-date-picker-grid td { padding: 1px; text-align: center; }.ui-date-picker-day { height: 36px; border-radius: var(--ui-radius-sm); font-size: 13px; font-variant-numeric: tabular-nums; cursor: pointer; outline: none; transition: background .1s;}.ui-date-picker-day:hover { background: var(--ui-subtle); }.ui-date-picker-day:focus-visible, .ui-date-picker-day:focus { box-shadow: var(--ui-focus); }.ui-date-picker-today { font-weight: 700; color: var(--ui-accent); box-shadow: inset 0 0 0 1px var(--ui-accent); }.ui-date-picker-day[aria-selected="true"] { background: var(--ui-accent); color: var(--ui-accent-fg); font-weight: 600; }.ui-date-picker-day[aria-disabled="true"] { color: var(--ui-muted); opacity: .45; text-decoration: line-through; cursor: not-allowed; background: none; }.ui-date-picker-foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 8px; }.ui-date-picker-link { padding: 4px 8px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-accent); font: 500 13px var(--ui-font); cursor: pointer;}.ui-date-picker-link:hover:not(:disabled) { background: var(--ui-accent-soft); }.ui-date-picker-link:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-date-picker-link:disabled { color: var(--ui-muted); cursor: not-allowed; }.ui-date-picker-keys { font-size: 12px; color: var(--ui-muted); }.ui-date-picker-hint, .ui-date-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-date-picker-error { color: var(--ui-danger); }.ui-date-picker-sm .ui-date-picker-box { height: 30px; width: 170px; }.ui-date-picker-sm .ui-date-picker-box input { font-size: 13px; }.ui-date-picker-sm .ui-date-picker-btn { width: 24px; height: 24px; }.ui-date-picker-disabled .ui-date-picker-box { opacity: .55; }@media (max-width: 420px) { .ui-date-picker-pop { right: 0; width: auto; max-width: 300px; } .ui-date-picker-day { height: 32px; } .ui-date-picker-keys { display: none; }}@media (prefers-reduced-motion: reduce) { .ui-date-picker-box, .ui-date-picker-day { transition: none; } }`let date_picker_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]let date_picker_weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]// A date as a day number: days since 1970-01-01 (H. Hinnant's days_from_civil).fn date_picker_days(y: int, m: int, d: int) -> int { let yy = m <= 2 ? y - 1 : y let era = floor(yy / 400) let yoe = yy - era * 400 let doy = floor((153 * ((m + 9) % 12) + 2) / 5) + d - 1 era * 146097 + yoe * 365 + floor(yoe / 4) - floor(yoe / 100) + doy - 719468}// A day number as { y, m, d }.fn date_picker_civil(z: int) { let zz = z + 719468 let era = floor(zz / 146097) let doe = zz - era * 146097 let yoe = floor((doe - floor(doe / 1460) + floor(doe / 36524) - floor(doe / 146096)) / 365) let doy = doe - (365 * yoe + floor(yoe / 4) - floor(yoe / 100)) let mp = floor((5 * doy + 2) / 153) let m = mp < 10 ? mp + 3 : mp - 9 { y: yoe + era * 400 + (m <= 2 ? 1 : 0), m: m, d: doy - floor((153 * mp + 2) / 5) + 1 }}fn date_picker_month_len(y: int, m: int) -> int = m == 2 ? (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1]// 0 for Sunday ... 6 for Saturday.fn date_picker_weekday(z: int) -> int = ((z % 7) + 11) % 7fn date_picker_pad(n: int) -> str = n < 10 ? `0${n}` : str(n)fn date_picker_iso(z: int) -> str { let c = date_picker_civil(z) `${c.y}-${date_picker_pad(c.m)}-${date_picker_pad(c.d)}`}// "YYYY-MM-DD" as a day number, or null when it is not a date.fn date_picker_parse(text: str?) { let t = trim(text ?? "") if !re.test(t, "^\\d{4}-\\d{2}-\\d{2}$") { return null } let y = int(slice(t, 0, 4)) let m = int(slice(t, 5, 7)) let d = int(slice(t, 8, 10)) m >= 1 && m <= 12 && d >= 1 && d <= date_picker_month_len(y, m) ? date_picker_days(y, m, d) : null}// The same day `k` months on (or back), kept inside the month: Jan 31 + 1 is Feb 28.fn date_picker_add_months(z: int, k: int) -> int { let c = date_picker_civil(z) let i = c.m - 1 + k let y = c.y + floor(i / 12) let m = i - floor(i / 12) * 12 + 1 let n = date_picker_month_len(y, m) date_picker_days(y, m, c.d > n ? n : c.d)}// The month holding day z, as weeks of seven day numbers (null outside the month).fn date_picker_weeks(z: int, week_start: int) -> list { let c = date_picker_civil(z) let first = date_picker_days(c.y, c.m, 1) let n = date_picker_month_len(c.y, c.m) let lead = (date_picker_weekday(first) - week_start + 7) % 7 range(ceil((lead + n) / 7)) | map((w) => range(7) | map((i) => { let at = w * 7 + i - lead at >= 0 && at < n ? first + at : null }))}fn date_picker_long(z: int) -> str { let c = date_picker_civil(z) `${date_picker_weekdays[date_picker_weekday(z)]} ${c.d} ${date_picker_months[c.m - 1]} ${c.y}`}component DatePicker(name: str? = null, label: str? = null, value: str? = null, min: str? = null, max: str? = null, disabled_dates: list = [], disabled_weekdays: list = [], week_start: int = 1, today: str? = null, placeholder: str = "YYYY-MM-DD", hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) { style global date_picker_css let lo = date_picker_parse(min) let hi = date_picker_parse(max) let blocked = disabled_dates | map((t) => date_picker_parse(t)) state chosen = date_picker_parse(value) state text = value ?? "" state now_d = date_picker_parse(today) ?? floor(now() / 86400000) state open = false state at = date_picker_parse(value) ?? date_picker_parse(today) ?? floor(now() / 86400000) state problem = "" let id = field ?? `date-picker-${name ?? re.replace(lower(label ?? "date"), "[^a-z0-9]+", "-")}` let shown = date_picker_civil(at) let weeks = date_picker_weeks(at, week_start) let first = date_picker_days(shown.y, shown.m, 1) let last_day = first + date_picker_month_len(shown.y, shown.m) - 1 let order = range(7) | map((i) => (i + week_start) % 7) let shown_error = problem != "" ? problem : error let described = ui_cls([hint != null && `${id}-hint`, shown_error != null && `${id}-error`]) let allowed = (z: int) => (lo == null || z >= lo) && (hi == null || z <= hi) && !contains(blocked, z) && !contains(disabled_weekdays, date_picker_weekday(z)) let kept = (z: int) => lo != null && z < lo ? lo : hi != null && z > hi ? hi : z let root_of = (e) => e.target.closest(".ui-date-picker") // Once the page shows the month, puts the focus on day `at` in it. let focus_day = (root) => { js("requestAnimationFrame", (t = null) => { let cell = root.querySelector(`[data-day="${at}"]`) cell && cell.focus() }) } let why = (z) => z == null ? "Type a date as YYYY-MM-DD, such as 2026-03-14." : lo != null && z < lo ? `Choose ${date_picker_iso(lo)} or later.` : hi != null && z > hi ? `Choose ${date_picker_iso(hi)} or earlier.` : "That date cannot be chosen." let set_chosen = (z) => { let before = chosen chosen = z problem = "" if z != before { on_change && on_change(z == null ? null : date_picker_iso(z)) } } let pick = (e, z: int) => { if allowed(z) { let root = root_of(e) at = z text = date_picker_iso(z) set_chosen(z) open = false let box = root.querySelector(".ui-date-picker-box input") box.setCustomValidity("") box.focus() } } let show = (e) => { if today == null { let d = js_new("Date") now_d = date_picker_days(d.getFullYear(), d.getMonth() + 1, d.getDate()) } at = kept(chosen ?? now_d) open = true focus_day(root_of(e)) } let go = (e, z: int) => { at = kept(z) focus_day(root_of(e)) } <div class={ui_cls(["ui-date-picker", size == "sm" && "ui-date-picker-sm", shown_error != null && "ui-date-picker-invalid", disabled && "ui-date-picker-disabled", class])} @focusout={(e) => { let root = root_of(e) if open && root != null && !(e.relatedTarget != null && root.contains(e.relatedTarget)) { open = false } }} @keydown={(e) => { if e.key == "Escape" && open { e.preventDefault() open = false root_of(e).querySelector(".ui-date-picker-btn").focus() } }}> {label != null && <label.ui-date-picker-label for={id}> {label}{required && <span.ui-date-picker-req aria-hidden="true">*</span>} </label>} <div.ui-date-picker-box> <input type="text" id={id} name={name} value={text} placeholder={placeholder} required={required} disabled={disabled} pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}" autocomplete="off" spellcheck="false" aria-invalid={shown_error != null ? "true" : null} aria-describedby={described != "" ? described : null} @input={(e) => { text = e.value let z = date_picker_parse(e.value) if trim(e.value) == "" { e.target.setCustomValidity("") set_chosen(null) } else if z != null && allowed(z) { e.target.setCustomValidity("") at = z set_chosen(z) } }} @change={(e) => { let z = date_picker_parse(e.value) if trim(e.value) == "" || (z != null && allowed(z)) { e.target.setCustomValidity("") problem = "" } else { set_chosen(null) problem = why(z) e.target.setCustomValidity(problem) } }} @keydown={(e) => { if e.key == "ArrowDown" && !open { e.preventDefault() show(e) } }}/> <button.ui-date-picker-btn type="button" disabled={disabled} aria-haspopup="dialog" aria-expanded={open ? "true" : "false"} aria-controls={`${id}-dialog`} aria-label={chosen != null ? `Choose date, ${date_picker_long(chosen)} is chosen` : "Choose date"} @click={(e) => { if open { open = false } else { show(e) } }}> <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="14" height="12.5" rx="2"/> <path d="M3 8.5h14M7 2.8v3.4M13 2.8v3.4"/></svg> </button> </div> <div.ui-date-picker-pop id={`${id}-dialog`} role="dialog" aria-modal="false" aria-labelledby={`${id}-month`} hidden={!open}> <div.ui-date-picker-head> <button.ui-date-picker-btn type="button" aria-label="Previous month" aria-disabled={lo != null && first <= lo ? "true" : null} @click={(e) => { if lo == null || first > lo { go(e, date_picker_add_months(at, -1)) } }}> <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5l-5 5 5 5"/></svg> </button> <h2 id={`${id}-month`} aria-live="polite">{date_picker_months[shown.m - 1]} {shown.y}</h2> <button.ui-date-picker-btn type="button" aria-label="Next month" aria-disabled={hi != null && last_day >= hi ? "true" : null} @click={(e) => { if hi == null || last_day < hi { go(e, date_picker_add_months(at, 1)) } }}> <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 5l5 5-5 5"/></svg> </button> </div> <table.ui-date-picker-grid role="grid" aria-labelledby={`${id}-month`} @keydown={(e) => { let back = (date_picker_weekday(at) - week_start + 7) % 7 let to = match e.key { "ArrowLeft" => at - 1 "ArrowRight" => at + 1 "ArrowUp" => at - 7 "ArrowDown" => at + 7 "Home" => at - back "End" => at + 6 - back "PageUp" => date_picker_add_months(at, e.shiftKey ? -12 : -1) "PageDown" => date_picker_add_months(at, e.shiftKey ? 12 : 1) _ => null } if to != null { e.preventDefault() go(e, to) } if e.key == "Enter" || e.key == " " { e.preventDefault() pick(e, at) } }}> <thead><tr>{for d in order => <th scope="col" abbr={date_picker_weekdays[d]}>{slice(date_picker_weekdays[d], 0, 2)}</th>}</tr></thead> <tbody> {for week in weeks => <tr> {for z in week => z == null ? <td role="gridcell"></td> : <td role="gridcell" data-day={z} tabindex={z == at ? "0" : "-1"} class={ui_cls(["ui-date-picker-day", z == now_d && "ui-date-picker-today"])} aria-label={date_picker_long(z)} aria-selected={z == chosen ? "true" : null} aria-current={z == now_d ? "date" : null} aria-disabled={allowed(z) ? null : "true"} @click={(e) => { at = z pick(e, z) }}>{date_picker_civil(z).d}</td>} </tr>} </tbody> </table> <div.ui-date-picker-foot> <button.ui-date-picker-link type="button" disabled={!allowed(now_d)} @click={(e) => pick(e, now_d)}>Today</button> <span.ui-date-picker-keys aria-hidden="true">Arrows move · Enter picks</span> </div> </div> {hint != null && <p.ui-date-picker-hint id={`${id}-hint`}>{hint}</p>} {shown_error != null && <p.ui-date-picker-error id={`${id}-error`} role="alert">{shown_error}</p>} </div>}Related
BirthdayInputDay, month and year drop-downs that give one date in a hidden input, the days following the month and the years the age allowed.5 views · 7 downloadsDateRangePickerA first and last day picked on two months (with a preview as you go) or from presets such as Last 7 days; or a stay in nights, around booked days, with a price under each.4 views · 6 downloadsMonthPickerA month and year picked from a grid of twelve months under the year, from a button or in the page, sent as YYYY-MM.4 views · 7 downloadsRecurrencePickerHow something repeats, daily, weekly on chosen days, monthly or yearly, and when it ends, said in words and sent as a rule and an RRULE.5 views · 7 downloadsWeekPickerAn ISO week picked from a month shown as numbered weeks, by click or the arrow keys, sent as 2026-W45.5 views · 8 downloadsYearPickerA year picked from a grid of a decade, turned ten years at a time, from a button or in the page.4 views · 7 downloads
Thread
Thread 0 posts
On DatePicker: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.