Forms

DateTimeInput

A date and a time side by side on the browser's own fields, giving one ISO value, in a time zone when given one.

#forms#input#calendar#time#interactive

Demo

Preview
Starts

Local time, as written.

Call
Time zone: Europe/Lisbon

Pick a date and a time.

Deploy at (UTC)
Time zone: UTC (Z)

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

Example

The code of the preview above.

demos/date_time_input.qwe
component DateTimeInputDemoLive() {    state at = null    <div.demo-stack>        <DateTimeInput name="dti-demo-call" label="Call" zone="Europe/Lisbon" step={15} on_change={(t) => { at = t }}/>        <p.demo-note>{at == null ? "Pick a date and a time." : `The form sends ${at}.`}</p>    </div>}view DateTimeInputDemo() {    <div.demo-stack>        <DateTimeInput name="dti-demo-starts" label="Starts" value="2026-11-02T09:00" earliest="2026-11-01" latest="2027-12-31"                       hint="Local time, as written."/>        <DateTimeInputDemoLive/>        <DateTimeInput name="dti-demo-utc" label="Deploy at (UTC)" zone="UTC" value="2026-11-02T14:30:00+01:00"                       required={true} error="That is in the past."/>    </div>}

Source

ui/date_time_input.qwe
// DateTimeInput: a moment picked as a date and a time side by side, on the// browser's own date and time fields (each phone's own pickers), giving one ISO// value. In a time zone it is the instant there, with its offset// ("2026-11-02T14:30:00Z", "2026-07-02T14:30:00+01:00": summer time included);// without one, the local date and time as written ("2026-11-02T14:30"). The value// goes in a hidden input named `name`, so a plain <form> sends it.////   <DateTimeInput name="starts" label="Starts" value="2026-11-02T09:00" earliest="2026-11-01"/>//   <DateTimeInput name="call" label="Call" zone="Europe/Lisbon" on_change={(t) => { at = t }}/>//// value: where it starts: "YYYY-MM-DDTHH:MM", or a date-time with an offset (shown// in `zone` when there is one). zone: an IANA zone ("Europe/Lisbon"), "UTC" or an// offset ("+05:30"); it is said under the fields. earliest, latest: the first and// last dates that can be chosen ("YYYY-MM-DD"). step: the minutes between times// the browser offers (1). name ("datetime"), label (the legend), hint, error,// required, disabled and field (the ids' start, "date-time-input-" + name by// default) as for every field. The value is empty until both parts are given.// on_change(value): inside one of your components, given the ISO value, or null.import ui/theme.qwe use ui_clslet date_time_input_css = css`.ui-date-time-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-date-time-input-label { padding: 0; font-size: 13px; font-weight: 500; }.ui-date-time-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-date-time-input-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }.ui-date-time-input-row input {  box-sizing: border-box; height: 38px; margin: 0; padding: 0 10px; 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); font-variant-numeric: tabular-nums;  transition: border-color .15s, box-shadow .15s;}.ui-date-time-input-row input:hover { border-color: var(--ui-strong); }.ui-date-time-input-row input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-date-time-input-row input:disabled { opacity: .55; cursor: not-allowed; }.ui-date-time-input-date { width: 160px; }.ui-date-time-input-time { width: 120px; }.ui-date-time-input-at { color: var(--ui-muted); font-size: 13px; }.ui-date-time-input-zone { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; }.ui-date-time-input-zone svg { width: 12px; height: 12px; }.ui-date-time-input-invalid .ui-date-time-input-row input { border-color: var(--ui-danger); }.ui-date-time-input-hint, .ui-date-time-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-date-time-input-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-date-time-input-row input { transition: none; } }`// The date and time parts ("YYYY-MM-DD", "HH:MM") of a value, shown in `zone` when// it has an offset and a zone is given; empty parts for what is not there.fn date_time_input_parts(value: str?, zone: str?) {    let v = trim(value ?? "")    if !re.test(v, "^\\d{4}-\\d{2}-\\d{2}") { return { d: "", t: "" } }    let with_offset = re.test(v, "(Z|[+-]\\d{2}:?\\d{2})$")    let local = with_offset && zone != null ? date.to_zone(v, zone) : v    let d = slice(local, 0, 10)    let t = re.test(local, "^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}") ? slice(local, 11, 16) : ""    { d: date.valid(d) ? d : "", t }}// The value for a date and a time: the instant in `zone`, or the local text.fn date_time_input_value(d: str, t: str, zone: str?) -> str =    d == "" || t == "" || !date.valid(d) ? "" : zone != null ? date.at(d, t, zone) : `${d}T${t}`component DateTimeInput(name: str = "datetime", label: str? = null, value: str? = null, zone: str? = null,                        earliest: str? = null, latest: str? = null, step: int = 1, hint: str? = null, error: str? = null,                        required: bool = false, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) {    style global date_time_input_css    state d = date_time_input_parts(value, zone).d    state t = date_time_input_parts(value, zone).t    let id = field ?? `date-time-input-${name}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`, zone != null && `${id}-zone`])    let iso = date_time_input_value(d, t, zone)    let set_parts = (nd: str, nt: str) => {        let before = date_time_input_value(d, t, zone)        d = nd        t = nt        let after = date_time_input_value(nd, nt, zone)        if after != before { on_change && on_change(after == "" ? null : after) }    }    <fieldset class={ui_cls(["ui-date-time-input", error != null && "ui-date-time-input-invalid", class])} id={id} disabled={disabled}              aria-describedby={described != "" ? described : null}>        {label != null && <legend.ui-date-time-input-label>            {label}{required && <span.ui-date-time-input-req aria-hidden="true">*</span>}        </legend>}        <div.ui-date-time-input-row>            <input.ui-date-time-input-date type="date" id={`${id}-date`} value={d} min={earliest} max={latest} required={required}                   aria-label="Date" aria-invalid={error != null ? "true" : null}                   @input={(e) => set_parts(e.value, t)} @change={(e) => set_parts(e.value, t)}/>            <span.ui-date-time-input-at aria-hidden="true">at</span>            <input.ui-date-time-input-time type="time" id={`${id}-time`} value={t} step={step * 60} required={required}                   aria-label="Time" aria-invalid={error != null ? "true" : null}                   @input={(e) => set_parts(d, slice(e.value, 0, 5))} @change={(e) => set_parts(d, slice(e.value, 0, 5))}/>            {zone != null && <span.ui-date-time-input-zone id={`${id}-zone`}>                <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true">                    <circle cx="6" cy="6" r="4.8"/><path d="M1.2 6h9.6M6 1.2c1.6 1.6 1.6 8 0 9.6M6 1.2c-1.6 1.6-1.6 8 0 9.6"/>                </svg>                <span><span.ui-sr>Time zone: </span>{zone}{iso != "" ? ` (${date.format(iso, "Z")})` : ""}</span>            </span>}        </div>        <input type="hidden" name={name} value={iso}/>        {hint != null && <p.ui-date-time-input-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-date-time-input-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
58Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.