BirthdayInput
Day, month and year drop-downs that give one date in a hidden input, the days following the month and the years the age allowed.
Demo
No date yet.
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/birthday_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/birthday_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/birthday_input.qwe
component BirthdayInputDemoLive() { state born = null <div.demo-stack> <BirthdayInput name="dob_live" label="When were you born?" field="birthday-input-demo-live" min_age={18} today="2026-10-28" on_change={(d) => { born = d }}/> <p.demo-note>{born == null ? "No date yet." : `Born on ${born}.`}</p> </div>}view BirthdayInputDemo() = <div.demo-stack> <BirthdayInput name="dob" label="Date of birth" value="1990-12-10" field="birthday-input-demo-plain" today="2026-10-28" hint="The days follow the month and year." required={true}/> <BirthdayInputDemoLive/> <div.demo-row> <BirthdayInput name="dob_us" label="US order" order="mdy" size="sm" field="birthday-input-demo-mdy" today="2026-10-28" years={30} error="Enter your date of birth."/> </div> </div>Source
ui/birthday_input.qwe
// BirthdayInput: a date of birth as three drop-downs, day, month and year, that// give one date: a hidden input named `name` holds it as "YYYY-MM-DD" (empty// until all three are chosen), so a plain <form> sends it. The days offered// follow the month and year (30 in April, 29 in a leap February), and the years// run back from the youngest allowed.//// <BirthdayInput name="dob" label="Date of birth"/>// <BirthdayInput name="dob" label="Date of birth" value="1990-12-10" min_age={18}// on_change={(d) => { birthday = d }}/>//// value: the date to start with. min_age: the youngest allowed, in years (0),// which sets the latest year; years: how many years back are offered (120).// today: the date the ages are counted from (today by default). order: "dmy"// (default), "mdy" or "ymd", the boxes' order. months: the month names// (English by default). hint, error, required, disabled, field (the ids' start,// default "birthday-input-" + name) and size ("sm" or "md") as for every field.// on_change(date): inside one of your components, given "YYYY-MM-DD", or null// while a part is missing.import ui/theme.qwe use ui_clslet birthday_input_css = css`.ui-birthday-input { display: grid; gap: 6px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-birthday-input-legend { padding: 0; font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-birthday-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-birthday-input-row { display: flex; flex-wrap: wrap; gap: 8px; }.ui-birthday-input-box { position: relative; display: block; }.ui-birthday-input-box::after { content: ""; position: absolute; right: 12px; top: 50%; width: 6px; height: 6px; margin-top: -5px; border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-birthday-input select { appearance: none; height: 38px; padding: 0 30px 0 12px; background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer; transition: border-color .15s, box-shadow .15s;}.ui-birthday-input-day select { width: 84px; }.ui-birthday-input-month select { width: 140px; }.ui-birthday-input-year select { width: 100px; }.ui-birthday-input select:hover { border-color: var(--ui-strong); }.ui-birthday-input select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-birthday-input select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-birthday-input option { color: var(--ui-fg); background: var(--ui-surface); }.ui-birthday-input-sm select { height: 30px; font-size: 13px; padding: 0 26px 0 10px; }.ui-birthday-input-invalid select { border-color: var(--ui-danger); }.ui-birthday-input-hint, .ui-birthday-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-birthday-input-error { color: var(--ui-danger); }.ui-birthday-input-disabled { opacity: .55; }.ui-birthday-input-disabled select { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-birthday-input select { transition: none; } }`let birthday_input_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]fn birthday_input_pad(n: int) -> str = n < 10 ? `0${n}` : str(n)// The year, month and day of "YYYY-MM-DD" as ints, or nulls for anything else.fn birthday_input_parts(text: str?) { let t = trim(text ?? "") if !re.test(t, "^\\d{4}-\\d{2}-\\d{2}$") { return { y: null, m: null, d: null } } { y: int(slice(t, 0, 4)), m: int(slice(t, 5, 7)), d: int(slice(t, 8, 10)) }}component BirthdayInput(name: str, label: str? = null, value: str? = null, min_age: int = 0, years: int = 120, today: str? = null, order: str = "dmy", months: list = birthday_input_months, 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 birthday_input_css let start = birthday_input_parts(value) state y = start.y state m = start.m state d = start.d let id = field ?? `birthday-input-${name}` let this_year = birthday_input_parts(today ?? date.today()).y let latest = this_year - min_age let year_list = range(years) | map((i) => latest - i) let days = y != null && m != null ? date.days_in_month(y, m) : 31 let day_ok = d != null && d <= days ? d : null let iso = y != null && m != null && day_ok != null ? `${y}-${birthday_input_pad(m)}-${birthday_input_pad(day_ok)}` : "" let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let told = () => { let dd = d != null && y != null && m != null && d <= date.days_in_month(y, m) ? d : null on_change && on_change(y != null && m != null && dd != null ? `${y}-${birthday_input_pad(m)}-${birthday_input_pad(dd)}` : null) } let pick = (e, part: str) => { let n = e.value == "" ? null : int(e.value) if part == "d" { d = n } else if part == "m" { m = n } else { y = n } told() } let boxes = { d: <span.ui-birthday-input-box.ui-birthday-input-day> <select id={`${id}-day`} aria-label="Day" required={required} disabled={disabled} autocomplete="bday-day" aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @change={(e) => pick(e, "d")}> <option value="" disabled selected={day_ok == null}>Day</option> {for i in range(days) => <option value={i + 1} selected={i + 1 == day_ok}>{i + 1}</option>} </select> </span>, m: <span.ui-birthday-input-box.ui-birthday-input-month> <select id={`${id}-month`} aria-label="Month" required={required} disabled={disabled} autocomplete="bday-month" aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @change={(e) => pick(e, "m")}> <option value="" disabled selected={m == null}>Month</option> {for [i, name_] in enumerate(months) => <option value={i + 1} selected={i + 1 == m}>{name_}</option>} </select> </span>, y: <span.ui-birthday-input-box.ui-birthday-input-year> <select id={`${id}-year`} aria-label="Year" required={required} disabled={disabled} autocomplete="bday-year" aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @change={(e) => pick(e, "y")}> <option value="" disabled selected={y == null}>Year</option> {for yr in year_list => <option value={yr} selected={yr == y}>{yr}</option>} </select> </span>, } <fieldset class={ui_cls(["ui-birthday-input", size == "sm" && "ui-birthday-input-sm", error != null && "ui-birthday-input-invalid", disabled && "ui-birthday-input-disabled", class])} id={id}> {label != null && <legend.ui-birthday-input-legend> {label}{required && <span.ui-birthday-input-req aria-hidden="true">*</span>} </legend>} <div.ui-birthday-input-row> {for p in split(order, "") => boxes[p]} </div> <input type="hidden" name={name} value={iso}/> {hint != null && <p.ui-birthday-input-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-birthday-input-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
CountryPickerA drop-down of every country with its flag and ISO code, favourites first, sending the two-letter code with a form.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 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 downloadsFontPickerA drop-down of fonts, each option drawn in itself, with a preview line in the one chosen.5 views · 7 downloadsLanguagePickerA drop-down of languages each written in its own name, sending the ISO 639-1 code with a form.5 views · 5 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 downloads
Thread
Thread 0 posts
On BirthdayInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.