Date and time pickers
DockedDateTimePicker is the docked date picker with time columns next to the calendar:
hours, minutes and, on a 12 hour locale, AM or PM. Use it whenever a day and a time belong to one decision,
such as scheduling a meeting or a shift.
value is a local YYYY-MM-DDTHH:mm string, built from local calendar fields
so it never shifts a day across a timezone boundary.
Usage
value is bindable and stays in sync with typing and calendar or column selection.
Picking in the panel is provisional until OK confirms it, and Cancel discards it.
The field only becomes a value once the typed text describes a whole moment, so a date without a
time leaves it empty. On a 12 hour clock the day period has to be typed too, since 08/17/2025, 07:30 could mean two different moments.
Value: 2025-08-17T14:30
<script lang="ts">
import { DockedDateTimePicker } from 'noph-ui'
let meeting = $state<string | undefined>('2025-08-17T14:30')
</script>
<DockedDateTimePicker bind:value={meeting} label="Starts at" />
<p>Value: <code>{meeting ?? 'undefined'}</code></p>
Below 600dp the columns move under the calendar instead of beside it, so the panel still fits a phone. The switch is a media query. Narrow the window to see it change.
Minutes and the clock
minuteStep sets how finely the minute column is cut, five minutes by default. It only
governs the column: a typed or preset minute is kept exactly as given. defaultTime is the time the columns open on while the field is empty, and hour12 overrides the clock the locale would choose.
Value: undefined
<script lang="ts">
import { DockedDateTimePicker } from 'noph-ui'
let shift = $state<string | undefined>()
</script>
<DockedDateTimePicker
bind:value={shift}
label="Shift start"
minuteStep={15}
defaultTime="09:00"
hour12={false}
/>
<p>Value: <code>{shift ?? 'undefined'}</code></p>
Localisation
Left to itself the clock follows the locale, including the order of fields, the digits, the
day-period names, and whether there is an AM/PM column at all. locale takes a BCP 47
tag, and firstDayOfWeek overrides the week start, exactly as on the date picker.
<script lang="ts">
import { DockedDateTimePicker } from 'noph-ui'
let germanMoment = $state<string | undefined>('2025-08-17T14:30')
</script>
<DockedDateTimePicker bind:value={germanMoment} locale="de-DE" label="Beginnt am" />
Bounding a moment
min and max take a whole moment here, not only a day. Days outside the
range are unselectable, and on the first and last day the hours and minutes outside it are greyed
out too. Picking such a day snaps the time to the nearest minute that day still allows. A bare YYYY-MM-DD still works: as min it means the start of that day, as max the end of it.
<script lang="ts">
import { DockedDateTimePicker } from 'noph-ui'
</script>
<DockedDateTimePicker
value="2025-08-18T10:00"
label="Appointment"
min="2025-08-18T09:30"
max="2025-08-20T16:00"
/>
isDateEnabled is called with the full moment rather than a bare day, so a rule can depend
on the time as well as the date.
Forms and validation
Passing a name submits the ISO moment with the surrounding form through a hidden
input, and validation stays on the visible field, so a blocked submit reports against a control
the browser can focus. The timing matches the date picker: validation
feedback appears on submit or blur, never while a moment is still being typed. issues replaces the supporting text with your own messages.
Nothing submitted yet.
<script lang="ts">
import { Button, DockedDateTimePicker } from 'noph-ui'
let formValue = $state<string | undefined>()
let formIssues = $state<{ message: string }[]>([])
let submitted = $state('')
const handleSubmit = (event: SubmitEvent) => {
event.preventDefault()
const data = new FormData(event.currentTarget as HTMLFormElement)
const value = data.get('startsAt')
formIssues = value ? [] : [{ message: 'Pick a start time.' }]
submitted = value ? `Submitted startsAt=${value}` : ''
}
</script>
<form onsubmit={handleSubmit} novalidate>
<DockedDateTimePicker
bind:value={formValue}
label="Starts at"
name="startsAt"
issues={formIssues}
required
/>
<Button type="submit" variant="filled">Submit</Button>
</form>
<p>{submitted || 'Nothing submitted yet.'}</p>
<style>
form {
display: flex;
align-items: flex-start;
gap: 1rem;
flex-wrap: wrap;
}
</style>
Shared with the date picker
The calendar half is the docked date picker, so its documentation applies here unchanged. See reacting to a change, text field variants, disabling individual days, restricting the year menu, days of neighbouring months , controlling the calendar and the show() and close() methods. For a day on its own, a modal, or a start and an end day, use DockedDatePicker, DatePickerDialog or DateRangePicker.
Theming
Colours and shapes come from the theme, and every part exposes a custom property for the cases the
theme cannot reach. Set them on the picker itself; they inherit into the calendar and the columns.
The calendar half also takes every --np-date-picker-* token.
| Property | Default |
|---|---|
--np-docked-date-time-picker-container-color | --np-color-surface-container-high |
--np-docked-date-time-picker-container-shape | --np-shape-corner-large |
--np-docked-date-time-picker-container-width | 22.5rem (360dp), the calendar side only |
--np-docked-date-time-picker-column-width | 4.5rem (72dp) |
--np-date-picker-time-selected-container-color | --np-color-secondary-container |
--np-date-picker-time-selected-label-color | --np-color-on-secondary-container |
Example
<script lang="ts">
import { DockedDateTimePicker } from 'noph-ui'
let themed = $state<string | undefined>('2025-08-17T14:30')
</script>
<DockedDateTimePicker
bind:value={themed}
label="Starts at"
--np-date-picker-time-selected-container-color="var(--np-color-tertiary)"
--np-date-picker-time-selected-label-color="var(--np-color-on-tertiary)"
--np-date-picker-date-selected-container-color="var(--np-color-tertiary)"
--np-date-picker-date-selected-label-color="var(--np-color-on-tertiary)"
--np-docked-date-time-picker-column-width="5rem"
/>
Motion and gestures
The calendar half moves exactly as the date picker does, on the
theme's own motion tokens. The month and year lists slide down over the calendar rather than
replacing it, so opening one leaves the panel the same size and the columns in place. The time
columns cross-fade the selected colour and open centred on the selected value. After that they
only scroll as far as you scroll them, so an in-progress pick is never pulled back to centre.
Every transition is wrapped in prefers-reduced-motion: no-preference.
Accessibility
Each time column is a role="listbox" of role="option" buttons with a
single roving tab stop, so the three columns are three stops in the tab order rather than over a
hundred. The selected option carries aria-selected, and an option outside min and max carries aria-disabled so it stays readable
rather than being skipped. hourLabel, minuteLabel and dayPeriodLabel name the columns.
| Key | Moves |
|---|---|
| ↑ ↓ | One option, wrapping at either end |
| Home End | First or last option of the column |
| Tab | The next column |
| Enter Space | Select the focused option |
| Esc | Close the panel |
The calendar keeps its own grid keyboard model, and every label string is a prop, so a localised app can translate the whole control.
API
It exports the same show() and close() as the other pickers. The
calendar lives in a popover with no id for a trigger to point at, so these are how a
page opens it; open reports whether it is showing. See the date picker's methods.
DockedDateTimePicker
Takes every DockedDatePicker prop, with value as a YYYY-MM-DDTHH:mm string, min and max as either a moment or a bare day, and isDateEnabled called with the
full moment. label defaults to 'Date and time', openCalendarLabel to 'Show date and time picker' and invalidDateMessage to 'Enter a valid date and time.' On top of those:
| Attribute | Type | Default | Description |
|---|---|---|---|
minuteStep | number | 5 | Spacing of the minute column. Typed and preset minutes are kept as they are. |
hour12 | boolean | from locale | Forces a 12 or 24 hour clock, and with it the AM/PM column. |
defaultTime | string | '00:00' | Time the columns open on while the field is still empty, as HH:mm. |
hourLabel / minuteLabel / dayPeriodLabel | string | 'Hour' / 'Minute' / 'AM or PM' | Accessible names of the three columns. |
Bindables
| Attribute | Type | Description |
|---|---|---|
value | string | null | undefined | Selected moment as YYYY-MM-DDTHH:mm. |
displayMonth | string | Month on screen, as an ISO day. Follows value until navigated, and is restored to
what you set when the panel closes. |
open | boolean | Whether the docked panel is showing. |
element | HTMLSpanElement | The picker's root element, which is the text field itself. |
Time helpers
The minute-precision maths behind the picker is exported too, so an app can share the same timezone-safe handling. The day-precision counterparts live with the date picker.
| Function | Description |
|---|---|
toISODateTime(date) / parseISODateTime(value, dateOnlyMinutes) | Serialises and parses YYYY-MM-DDTHH:mm from local fields. Parsing also takes a bare
day, landing it on the minute given. |
parseDateTimeInput(text, locale, hour12) / formatDateTime | Typed text and formatted output for a whole moment. |
formatTime(date, locale, hour12) / getDateTimePattern(locale) | The time on its own, and the MM/DD/YYYY, hh:mm AM style hint of a locale. |
uses12HourClock(locale) / getHourLabels / getDayPeriodLabels | The clock of a locale, and the labels the time columns are built from. |
minutesOfDay / withMinutes / toISOTime / compareTimes / isTimeWithin | Minute-precision maths on a moment. |