DateRangePicker
A 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.
Demo
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 |
January 2027
| 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 |
8 days: 20 Dec – 27 Dec 2026.
Pick the day you arrive, then the day you leave.
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 |
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 |
Pick the first day.
Nothing chosen yet.
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€140 | 21€140 | 22€95 |
| 23€95 | 24€95 | 25€95 | 26€95 | 27€140 | 28€140 | 29€95 |
| 30€95 |
December 2026
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| 1€95 | 2€95 | 3€95 | 4€140 | 5€140 | 6€95 | |
| 7€95 | 8€95 | 9€95 | 10€95 | 11€140 | 12€140 | 13€95 |
| 14€95 | 15€95 | 16€95 | 17€95 | 18€140 | 19€140 | 20€95 |
| 21€95 | 22€95 | 23€95 | 24€95 | 25€140 | 26€140 | 27€95 |
| 28€95 | 29€95 | 30€95 | 31€95 |
Pick the first day.
Two nights or more. Crossed out: booked; a stay can end on one.
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 |
December 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 | 31 |
Pick the first day.
Choose a whole quarter.
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_range_picker.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/date_range_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_range_picker.qwe
component DateRangePickerDemoLive() { state picked = "Nothing chosen yet." <div.demo-stack> <DateRangePicker name="drp-demo-report" label="Report period" on_change={(a, b) => { picked = a == null ? "Cleared." : `The form sends ${a} to ${b}.` }}/> <p.demo-note>{picked}</p> </div>}// A price under each day: €140 on Friday and Saturday nights, €95 the others.fn date_range_picker_demo_prices() -> map { let from = date_range_picker_days(2026, 11, 20) range(72) | reduce({}, (m, i) => set(m, date_range_picker_iso(from + i), date_range_picker_weekday(from + i) >= 5 ? "€140" : "€95"))}view DateRangePickerDemo() = <div.demo-stack> <DateRangePicker name="drp-demo-stay" label="Your stay" start="2026-12-20" end="2026-12-27" min="2026-11-01" max="2027-03-31" presets={[]} hint="Pick the day you arrive, then the day you leave."/> <DateRangePickerDemoLive/> <DateRangePicker name="drp-demo-nights" label="A stay, in nights" today="2026-11-20" min="2026-11-20" presets={[]} min_nights={2} disabled_dates={["2026-12-04", "2026-12-05", "2026-12-12"]} day_labels={date_range_picker_demo_prices()} hint="Two nights or more. Crossed out: booked; a stay can end on one."/> <DateRangePicker name="drp-demo-q" names={["period[start]", "period[end]"]} label="Quarter" size="sm" week_start={0} error="Choose a whole quarter." presets={[{ label: "Q3 2026", start: "2026-07-01", end: "2026-09-30" }, { label: "Q4 2026", start: "2026-10-01", end: "2026-12-31" }]}/> </div>Source
ui/date_range_picker.qwe
// DateRangePicker: a span of days, from a first to a last, picked on two months// side by side or with a preset ("Last 7 days", "This month"). Click the first day// then the last; between the two clicks the days that would be chosen are shown// as the pointer (or the focus) moves. The calendars work like DatePicker's: the// arrow keys by day and week, PageUp / PageDown by month, Home / End, Enter or// Space to pick, Escape to close. A plain <form> gets "YYYY-MM-DD" as// `name`_start and `name`_end.//// <DateRangePicker name="stay" label="Your stay" start="2026-12-20" end="2026-12-27"/>// <DateRangePicker name="report" label="Period" max="2026-12-31"// presets={[{ label: "Q4 2026", start: "2026-10-01", end: "2026-12-31" }]}/>// <DateRangePicker label="Dates" on_change={(a, b) => { range = [a, b] }}/>//// start, end: "YYYY-MM-DD" to start with. min, max: the first and last days that// can be chosen. presets: a list of { label, start, end } shown beside the// calendars; by default Today, Last 7 days, Last 30 days, This month, Last month// and This year, counted from today ([] for none). names: the two names sent,// [start name, end name] (`name`_start and `name`_end; ["stay[start]",// "stay[end]"] works too). week_start: the first column (1, Monday; 0 for Sunday).// today: the day counted from and marked (the browser's own date by default).// placeholder: the button's text while nothing is chosen ("Choose dates").// name, label, hint, error, disabled, field (the button's id, "date-range-picker-"// + name by default) and size ("sm" or "md") as for every field.// on_change(start, end): inside one of your components, given both "YYYY-MM-DD"// once the last day is picked (or both null when cleared).// disabled_dates: days that cannot be picked, crossed out ("YYYY-MM-DD" each), as// DatePicker's; a range cannot run across one. day_labels: a short text under each// day it names ({ "2026-12-24": "€140" }: prices, say). min_nights: counts nights// instead of days, for a stay: the end is the day one leaves, at least min_nights// after the start, and it may be a disabled day (its night is not part of the stay).//// <DateRangePicker name="stay" label="Dates" min_nights={2} disabled_dates={booked}// day_labels={prices}/>import ui/theme.qwe use ui_clslet date_range_picker_css = css`.ui-date-range-picker { position: relative; display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-date-range-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-date-range-picker-trigger { display: flex; align-items: center; gap: 8px; width: fit-content; min-width: 240px; max-width: 100%; height: 38px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); text-align: left; cursor: pointer; transition: border-color .15s, box-shadow .15s;}.ui-date-range-picker-trigger svg { flex: none; width: 18px; height: 18px; color: var(--ui-muted); }.ui-date-range-picker-trigger:hover { border-color: var(--ui-strong); }.ui-date-range-picker-trigger:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-date-range-picker-trigger:disabled { opacity: .55; cursor: not-allowed; }.ui-date-range-picker-empty { color: var(--ui-muted); }.ui-date-range-picker-invalid .ui-date-range-picker-trigger { border-color: var(--ui-danger); }.ui-date-range-picker-pop { position: absolute; z-index: 30; top: 100%; left: 0; width: max-content; 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-range-picker-pop[hidden] { display: none; }.ui-date-range-picker-body { display: flex; gap: 16px; }.ui-date-range-picker-presets { display: flex; flex-direction: column; gap: 2px; min-width: 120px; padding-right: 12px; border-right: 1px solid var(--ui-border); }.ui-date-range-picker-preset { padding: 6px 10px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); font: 13px var(--ui-font); text-align: left; white-space: nowrap; cursor: pointer;}.ui-date-range-picker-preset:hover { background: var(--ui-subtle); }.ui-date-range-picker-preset[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent); font-weight: 500; }.ui-date-range-picker-preset:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-date-range-picker-months { display: flex; flex-wrap: wrap; gap: 20px; }.ui-date-range-picker-month { width: 252px; max-width: 100%; }.ui-date-range-picker-head { display: flex; align-items: center; justify-content: center; position: relative; height: 30px; margin-bottom: 4px; }.ui-date-range-picker-head h2 { margin: 0; font-size: 14px; font-weight: 600; }.ui-date-range-picker-nav { position: absolute; top: 0; display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-date-range-picker-nav svg { width: 18px; height: 18px; }.ui-date-range-picker-prev { left: 0; }.ui-date-range-picker-next { right: 0; }.ui-date-range-picker-nav:hover:not([aria-disabled="true"]) { background: var(--ui-subtle); color: var(--ui-fg); }.ui-date-range-picker-nav:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-date-range-picker-nav[aria-disabled="true"] { opacity: .35; cursor: not-allowed; }.ui-date-range-picker-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }.ui-date-range-picker-grid th { padding: 4px 0; font-size: 12px; font-weight: 500; color: var(--ui-muted); text-align: center; }.ui-date-range-picker-grid td { padding: 1px 0; text-align: center; }.ui-date-range-picker-day { height: 34px; font-size: 13px; font-variant-numeric: tabular-nums; cursor: pointer; outline: none; }.ui-date-range-picker-day:hover { background: var(--ui-subtle); border-radius: var(--ui-radius-sm); }.ui-date-range-picker-day:focus { box-shadow: var(--ui-focus); border-radius: var(--ui-radius-sm); }.ui-date-range-picker-today { font-weight: 700; color: var(--ui-accent); }.ui-date-range-picker-in, .ui-date-range-picker-day.ui-date-range-picker-in:hover { background: var(--ui-accent-soft); color: var(--ui-fg); border-radius: 0; }.ui-date-range-picker-preview { background: color-mix(in srgb, var(--ui-accent-soft) 60%, transparent); box-shadow: inset 0 1px 0 var(--ui-accent), inset 0 -1px 0 var(--ui-accent); }.ui-date-range-picker-edge, .ui-date-range-picker-day.ui-date-range-picker-edge:hover { background: var(--ui-accent); color: var(--ui-accent-fg); font-weight: 600; }.ui-date-range-picker-first { border-radius: var(--ui-radius-sm) 0 0 var(--ui-radius-sm); }.ui-date-range-picker-last { border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0; }.ui-date-range-picker-first.ui-date-range-picker-last { border-radius: var(--ui-radius-sm); }.ui-date-range-picker-day[aria-disabled="true"] { color: var(--ui-muted); opacity: .45; cursor: not-allowed; background: none; }.ui-date-range-picker-off { text-decoration: line-through; }.ui-date-range-picker-note { display: block; font-size: 10px; line-height: 1.1; color: var(--ui-muted); text-decoration: none; }.ui-date-range-picker-noted .ui-date-range-picker-day { height: 42px; line-height: 1.2; }.ui-date-range-picker-edge .ui-date-range-picker-note { color: inherit; }.ui-date-range-picker-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--ui-border); font-size: 13px; color: var(--ui-muted); }.ui-date-range-picker-clear { padding: 4px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); font: 500 13px var(--ui-font); cursor: pointer;}.ui-date-range-picker-clear:hover { border-color: var(--ui-strong); }.ui-date-range-picker-clear:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-date-range-picker-hint, .ui-date-range-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-date-range-picker-error { color: var(--ui-danger); }.ui-date-range-picker-sm .ui-date-range-picker-trigger { height: 30px; min-width: 200px; font-size: 13px; }@media (max-width: 640px) { .ui-date-range-picker-body { flex-direction: column; gap: 10px; } .ui-date-range-picker-presets { flex-direction: row; flex-wrap: wrap; padding: 0 0 10px; border-right: 0; border-bottom: 1px solid var(--ui-border); } .ui-date-range-picker-month { width: 100%; } .ui-date-range-picker-pop { right: 0; width: auto; max-width: none; }}@media (prefers-reduced-motion: reduce) { .ui-date-range-picker-trigger { transition: none; } }`let date_range_picker_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]let date_range_picker_weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]// The date helpers are DatePicker's, so that each file works on its own: a date is// a day number, the days since 1970-01-01.fn date_range_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}fn date_range_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_range_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]fn date_range_picker_weekday(z: int) -> int = ((z % 7) + 11) % 7fn date_range_picker_pad(n: int) -> str = n < 10 ? `0${n}` : str(n)fn date_range_picker_iso(z: int) -> str { let c = date_range_picker_civil(z) `${c.y}-${date_range_picker_pad(c.m)}-${date_range_picker_pad(c.d)}`}fn date_range_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_range_picker_month_len(y, m) ? date_range_picker_days(y, m, d) : null}// The first day of the month `k` months after the one holding day z.fn date_range_picker_month(z: int, k: int) -> int { let c = date_range_picker_civil(z) let i = c.m - 1 + k date_range_picker_days(c.y + floor(i / 12), i - floor(i / 12) * 12 + 1, 1)}fn date_range_picker_add_months(z: int, k: int) -> int { let c = date_range_picker_civil(z) let f = date_range_picker_civil(date_range_picker_month(z, k)) let n = date_range_picker_month_len(f.y, f.m) date_range_picker_days(f.y, f.m, c.d > n ? n : c.d)}fn date_range_picker_month_end(z: int) -> int { let c = date_range_picker_civil(z) date_range_picker_days(c.y, c.m, date_range_picker_month_len(c.y, c.m))}fn date_range_picker_weeks(first: int, week_start: int) -> list { let c = date_range_picker_civil(first) let n = date_range_picker_month_len(c.y, c.m) let lead = (date_range_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_range_picker_long(z: int) -> str { let c = date_range_picker_civil(z) `${date_range_picker_weekdays[date_range_picker_weekday(z)]} ${c.d} ${date_range_picker_months[c.m - 1]} ${c.y}`}// "2 Nov – 8 Nov 2026", or with both years when they differ.fn date_range_picker_span(a: int, b: int) -> str { let x = date_range_picker_civil(a) let y = date_range_picker_civil(b) let short = (c) => `${c.d} ${slice(date_range_picker_months[c.m - 1], 0, 3)}` x.y == y.y ? `${short(x)} – ${short(y)} ${y.y}` : `${short(x)} ${x.y} – ${short(y)} ${y.y}`}// The presets offered when none are given, counted from day t.fn date_range_picker_presets(t: int) -> list { let c = date_range_picker_civil(t) let this_month = date_range_picker_days(c.y, c.m, 1) let last_month = date_range_picker_month(t, -1) [ { label: "Today", start: t, end: t }, { label: "Last 7 days", start: t - 6, end: t }, { label: "Last 30 days", start: t - 29, end: t }, { label: "This month", start: this_month, end: t }, { label: "Last month", start: last_month, end: this_month - 1 }, { label: "This year", start: date_range_picker_days(c.y, 1, 1), end: t }, ]}component DateRangePicker(name: str? = null, label: str? = null, start: str? = null, end: str? = null, min: str? = null, max: str? = null, presets: list? = null, names: list? = null, week_start: int = 1, today: str? = null, placeholder: str = "Choose dates", hint: str? = null, error: str? = null, disabled: bool = false, field: str? = null, size: str = "md", disabled_dates: list = [], day_labels: map = {}, min_nights: int? = null, on_change = null, class: str? = null) { style global date_range_picker_css let lo = date_range_picker_parse(min) let hi = date_range_picker_parse(max) let first_a = date_range_picker_parse(start) let first_b = date_range_picker_parse(end) let both = first_a != null && first_b != null && first_a <= first_b // from_d / to_d: the range chosen, shown and sent; a / b: the one being picked in the calendars. state from_d = both ? first_a : null state to_d = both ? first_b : null state a = both ? first_a : null state b = both ? first_b : null state now_d = date_range_picker_parse(today) ?? floor(now() / 86400000) state at = both ? first_a : date_range_picker_parse(today) ?? floor(now() / 86400000) state left = date_range_picker_month(both ? first_a : date_range_picker_parse(today) ?? floor(now() / 86400000), 0) state open = false state hover = null let id = field ?? `date-range-picker-${name ?? re.replace(lower(label ?? "dates"), "[^a-z0-9]+", "-")}` let sent = names ?? (name != null ? [`${name}_start`, `${name}_end`] : [null, null]) let offered = presets != null ? presets | map((p) => { label: p.label, start: date_range_picker_parse(p.start), end: date_range_picker_parse(p.end) }) | filter((p) => p.start != null && p.end != null) : date_range_picker_presets(now_d) let right = date_range_picker_month(left, 1) let left_end = date_range_picker_month_end(right) let order = range(7) | map((i) => (i + week_start) % 7) let picking = a != null && b == null let tip = picking ? (hover ?? at) : null let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let allowed = (z: int) => (lo == null || z >= lo) && (hi == null || z <= hi) let blocked = disabled_dates | map((t) => date_range_picker_parse(t)) | filter((z) => z != null) let nights = min_nights != null let free = (z: int) => allowed(z) && !contains(blocked, z) // While the end is picked: the first disabled day after the start, which the // range cannot pass (in nights, it may end on it). let after = picking ? blocked | filter((z) => z > a) | sort : [] let wall = len(after) > 0 ? after[0] : null let end_ok = (z: int) => allowed(z) && (wall == null || (nights ? z <= wall : z < wall)) && (nights ? z - a >= min_nights : !contains(blocked, z)) let can = (z: int) => picking && z >= a ? end_ok(z) : free(z) // No disabled day inside x..y (but the last, in nights). let clear_span = (x: int, y: int) => len(blocked | filter((z) => z >= x && (nights ? z < y : z <= y))) == 0 let kept = (z: int) => lo != null && z < lo ? lo : hi != null && z > hi ? hi : z let root_of = (e) => e.target.closest(".ui-date-range-picker") let focus_day = (root) => { js("requestAnimationFrame", (t = null) => { let cell = root.querySelector(`[data-day="${at}"]`) cell && cell.focus() }) } // Moves the focus to day z, turning the months so that it shows. let go = (e, z: int) => { let to = kept(z) at = to if to < left { left = date_range_picker_month(to, 0) } if to > left_end { left = date_range_picker_month(to, -1) } if picking { hover = null } focus_day(root_of(e)) } let done = (e, x, y) => { a = x b = y from_d = x to_d = y hover = null open = false root_of(e).querySelector(".ui-date-range-picker-trigger").focus() on_change && on_change(x == null ? null : date_range_picker_iso(x), y == null ? null : date_range_picker_iso(y)) } let pick = (e, z: int) => { if picking && z >= a { if end_ok(z) { at = z done(e, a, z) } } else if free(z) { at = z a = z if !picking { b = null hover = null } } } let show = (e) => { if today == null { let d = js_new("Date") now_d = date_range_picker_days(d.getFullYear(), d.getMonth() + 1, d.getDate()) } a = from_d b = to_d hover = null at = kept(from_d ?? now_d) left = date_range_picker_month(at, 0) open = true focus_day(root_of(e)) } let status = a == null ? "Pick the first day." : b == null ? (nights ? `From ${date_range_picker_long(a)}: now pick the day it ends, ${min_nights} ${min_nights == 1 ? "night" : "nights"} or more later.` : `From ${date_range_picker_long(a)}: now pick the last day.`) : nights ? `${b - a} ${b - a == 1 ? "night" : "nights"}: ${date_range_picker_span(a, b)}.` : `${b - a + 1} ${b == a ? "day" : "days"}: ${date_range_picker_span(a, b)}.` // The preview stops where the range would have to stop. let reach = tip != null && wall != null && tip > wall ? (nights ? wall : wall - 1) : tip <div class={ui_cls(["ui-date-range-picker", size == "sm" && "ui-date-range-picker-sm", error != null && "ui-date-range-picker-invalid", len(keys(day_labels)) > 0 && "ui-date-range-picker-noted", 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-range-picker-trigger").focus() } }}> {label != null && <label.ui-date-range-picker-label id={`${id}-label`} for={id}>{label}</label>} <button.ui-date-range-picker-trigger type="button" id={id} disabled={disabled} aria-haspopup="dialog" aria-expanded={open ? "true" : "false"} aria-controls={`${id}-dialog`} aria-labelledby={label != null ? `${id}-label ${id}-text` : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @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> <span id={`${id}-text`} class={from_d == null ? "ui-date-range-picker-empty" : null}> {from_d != null && to_d != null ? date_range_picker_span(from_d, to_d) : placeholder} </span> </button> <div.ui-date-range-picker-pop id={`${id}-dialog`} role="dialog" aria-modal="false" aria-label={label != null ? `${label}: choose dates` : "Choose dates"} hidden={!open}> <div.ui-date-range-picker-body> {len(offered) > 0 && <div.ui-date-range-picker-presets role="group" aria-label="Presets"> {for p in offered => <button.ui-date-range-picker-preset type="button" key={p.label} aria-pressed={p.start == a && p.end == b ? "true" : "false"} disabled={!allowed(p.start) || !allowed(p.end) || !clear_span(p.start, p.end)} @click={(e) => { at = p.start left = date_range_picker_month(p.start, 0) done(e, p.start, p.end) }}>{p.label}</button>} </div>} <div.ui-date-range-picker-months @pointerleave={() => { hover = null }}> {for [k, first] in enumerate([left, right]) => <div.ui-date-range-picker-month key={k}> <div.ui-date-range-picker-head> {k == 0 && <button.ui-date-range-picker-nav.ui-date-range-picker-prev type="button" aria-label="Previous month" aria-disabled={lo != null && left <= lo ? "true" : null} @click={() => { if lo == null || left > lo { left = date_range_picker_month(left, -1) at = kept(date_range_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}-m${k}`} aria-live={k == 0 ? "polite" : null}> {date_range_picker_months[date_range_picker_civil(first).m - 1]} {date_range_picker_civil(first).y} </h2> {k == 1 && <button.ui-date-range-picker-nav.ui-date-range-picker-next type="button" aria-label="Next month" aria-disabled={hi != null && left_end >= hi ? "true" : null} @click={() => { if hi == null || left_end < hi { left = date_range_picker_month(left, 1) at = kept(date_range_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-range-picker-grid role="grid" aria-labelledby={`${id}-m${k}`} @keydown={(e) => { let back = (date_range_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_range_picker_add_months(at, e.shiftKey ? -12 : -1) "PageDown" => date_range_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) if open { focus_day(root_of(e)) } } }}> <thead><tr>{for d in order => <th scope="col" abbr={date_range_picker_weekdays[d]}>{slice(date_range_picker_weekdays[d], 0, 2)}</th>}</tr></thead> <tbody> {for week in date_range_picker_weeks(first, week_start) => <tr> {for z in week => z == null ? <td role="gridcell"></td> : { let lo_end = a ?? z let hi_end = b ?? reach ?? a ?? z let inside = a != null && z >= lo_end && z <= hi_end let edge = z == a || z == b let note = get(day_labels, date_range_picker_iso(z)) <td role="gridcell" data-day={z} tabindex={z == at ? "0" : "-1"} class={ui_cls(["ui-date-range-picker-day", z == now_d && "ui-date-range-picker-today", contains(blocked, z) && "ui-date-range-picker-off", inside && "ui-date-range-picker-in", inside && picking && !edge && "ui-date-range-picker-preview", edge && "ui-date-range-picker-edge", inside && (z == lo_end || date_range_picker_weekday(z) == week_start) && "ui-date-range-picker-first", inside && (z == hi_end || date_range_picker_weekday(z) == (week_start + 6) % 7) && "ui-date-range-picker-last"])} aria-label={contains(blocked, z) ? `${date_range_picker_long(z)}, unavailable` : date_range_picker_long(z)} aria-selected={a != null && z >= a && z <= (b ?? a) ? "true" : null} aria-current={z == now_d ? "date" : null} aria-disabled={can(z) || z == a ? null : "true"} @pointerenter={() => { if picking { hover = z } }} @click={(e) => { pick(e, z) if open { focus_day(root_of(e)) } }}>{date_range_picker_civil(z).d}{note != null ? <span.ui-date-range-picker-note>{note}</span> : null}</td> }} </tr>} </tbody> </table> </div>} </div> </div> <div.ui-date-range-picker-foot> <span aria-live="polite">{status}</span> <button.ui-date-range-picker-clear type="button" @click={(e) => { a = null b = null hover = null if from_d != null { from_d = null to_d = null on_change && on_change(null, null) } }}>Clear</button> </div> </div> {sent[0] != null && <input type="hidden" name={sent[0]} value={from_d != null ? date_range_picker_iso(from_d) : ""}/>} {sent[1] != null && <input type="hidden" name={sent[1]} value={to_d != null ? date_range_picker_iso(to_d) : ""}/>} {hint != null && <p.ui-date-range-picker-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-date-range-picker-error id={`${id}-error`} role="alert">{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 downloadsDatePickerA date typed as YYYY-MM-DD or picked in a calendar grid, with the keyboard, limits and days that cannot be chosen.7 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 DateRangePicker: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.