Forms

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.

#forms#select#picker#calendar#interactive

Demo

Preview
Date of birth

The days follow the month and year.

When were you born?

No date yet.

US order

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.qwe
main.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>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.