Form controls
DateTimePicker
DatePicker and TimePicker as one field, with one label, one hint and one error between them. The value is a local wall clock with no zone, and that is the part a reader gets wrong, so every example below prints what the field is holding.
Basic
The label points at the date trigger, which is a button, so clicking the label opens the calendar. Pick a day and set the clock, and watch the value under the field.
2026-03-04T15:30Under a touch screen the date half takes the DatePicker's 44px days in a 320px calendar, and the time half takes the TimePicker's 44px segments. A press on the time half's padding or a colon focuses the nearest segment.
The wire format
There are exactly three shapes the value can take.
- Unset is the empty string.
- Without
secondsit is2026-03-04T15:30. - With
secondsit is2026-03-04T15:30:45.
Four digit year, two digit zero padded everything else, a T between the halves. No
offset, no trailing Z, no fraction. A string that is none of those three empties both
halves rather than being guessed at, and a bare 2026-03-04 is read as that day with no
time, because that is what a date field's own value carries.
The value carries no offset on purpose. A datetime column is zone aware on PostgreSQL and zone blind on MySQL and SQL Server, so an offset written into the value is kept by one and dropped by the others and one string names a different instant depending on where it lands. The offset a browser can supply is the viewer's own, not the zone the content is scheduled against, so an editor in Jakarta and an editor in New York would write two different strings for the same intent. Converting a wall clock to an absolute instant belongs at the boundary that knows which zone to apply.
A day picked before the clock is touched publishes as that day at midnight, so the calendar never marks a day the page does not hold. A clock reading with no day publishes nothing, because there is no instant to name yet.
Seconds
2026-03-04T15:30:45Twelve hour
hour12 changes only what the time half draws. The hour segment runs 1 to 12 beside an AM
and PM segment that a click, an arrow key, or A or P sets. The value stays 24-hour, so a form posts
the same string either way.
2026-03-05T14:15Bounds
min and max bound the whole instant, never a half on its own. With a
floor of 4 March at 09:00 and a ceiling of 6 March at 17:00, eight in the morning on the fifth is
inside the range and is left alone. A time bound reaches the time half only on the day that bound
falls on. A bare date as a bound means the whole of that day: a min starts at its
midnight and a max runs to its last second, so the last allowed day is a day the user can
actually use.
Between 4 March 09:00 and 6 March 17:00.
(empty)In a form
Neither half carries a name. name goes on one hidden input holding the whole value, so
a submit posts the instant and not the two strings it was built from. The label, the hint and the error
belong to the field rather than to either half, which is what keeps three message rows from stacking
under one control.
API
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | '' | Bindable local wall clock: YYYY-MM-DDTHH:mm, or with seconds YYYY-MM-DDTHH:mm:ss. |
label | string | no default | Field label. Points at the date trigger. |
hint | string | no default | Helper text below the field. An error replaces it. |
error | string | no default | Error message, reported through the group description. |
seconds | boolean | false | Add a seconds segment, and carry seconds in the value. |
hour12 | boolean | false | Draw the time half 12-hour with an AM and PM segment. The value stays 24-hour. |
step | number | 1 | Arrow-key step for the minute and second segments on the time half. |
min | string | no default | Earliest allowed instant. A bare date means its midnight. |
max | string | no default | Latest allowed instant. A bare date means its last second. |
required | boolean | false | Mark as required for form validation. Renders a red asterisk. |
disabled | boolean | false | Disable both halves. |
id | string | no default | DOM id the half ids are built from. Generated when omitted. |
name | string | no default | Native form name. One hidden input carries the whole instant. |
class | string | no default | Extra classes on the field wrapper. |
onchange | (value: string) => void | no default | Fires whenever the published value changes. |
noAI | boolean | false | Keep the value from an assistant: a FormAssist never reports it or fills it, and the control is marked data-no-ai for a browser agent. |