Date and time pickers

DockedDateTimePicker is the docked date picker with the time next to the calendar: a column of hours, one of minutes and, on a 12 hour locale, one for AM and PM. It is the shape a browser gives an <input type="datetime-local">, and it is deliberately not the Material clock dial, which is built for a thumb rather than a pointer. Reach for it when the day and the time belong to one decision, like the start of a meeting or a shift.

value is a local YYYY-MM-DDTHH:mm string, the day-precision YYYY-MM-DD of the other pickers extended by a time. It is built from local calendar fields, so it never slips a day across a timezone boundary.

Usage

value is bindable. Typing in the field and picking a day and a time stay in sync; inside the panel the selection is provisional until OK confirms it, and Cancel discards it. Everything the date side does is unchanged, including typing in the field, and the time is confirmed by the same OK.

The field holds the value in the format of the locale, and it only becomes a value once the text describes a whole moment, so a date with no time yet leaves it empty. On a 12 hour clock the day period has to be typed too: 08/17/2025, 07:30 is two different moments, and picking one silently is worse than waiting for the rest.

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" />

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, like the range picker's. 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 minute typed into the field is taken exactly as it is, and a value the app sets keeps whatever minute it holds. defaultTime is the time a first pick starts from when the field is still empty, and hour12 overrides the clock the locale would choose.

Value: undefined

<DockedDateTimePicker
	bind:value={shift}
	label="Shift start"
	minuteStep={15}
	defaultTime="09:00"
	hour12={false}
/>

Localisation

Left to itself the clock follows the locale, and so does everything around it: the order of the fields, the digits, the names of the day periods and whether there is a third column at all. locale takes a BCP 47 tag, and firstDayOfWeek overrides the week start the locale implies, exactly as on the date picker.

<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 the last day the hours and minutes outside it are greyed out too. Picking such a day pulls the time to the nearest minute that day still allows, so the selection is never a moment the bounds refuse. A bare YYYY-MM-DD still works: as min it means the start of that day, as max the end of it.

<DockedDateTimePicker
	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 turn 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 the constraints stay on the visible field, so a blocked submit reports against a control the browser can focus. The timing is the same as on the date picker: :user-invalid switches on at a submit attempt and on blur, never while a moment is still being typed. issues replaces the supporting text with your own messages.

Nothing submitted yet.

<form onsubmit={handleSubmit} novalidate>
	<DockedDateTimePicker
		bind:value={formValue}
		label="Starts at"
		name="startsAt"
		issues={formIssues}
		required
	/>
	<Button type="submit" variant="filled">Submit</Button>
</form>

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 and controlling the calendar. 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.

PropertyDefault
--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-width22.5rem (360dp), the calendar side only
--np-docked-date-time-picker-column-width4.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

<DockedDateTimePicker
	bind:value
	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, and the month and year lists slide down over the calendar rather than in its place, so opening one leaves the panel the same size and the columns beside it where they are. The time columns add nothing of their own beyond a colour cross-fade on the selection: they scroll, and they open centred on the selected value so the values around it are in view. After that they only scroll as far as they have to, because pulling the list back to the middle under a pointer that is still picking makes the next click land somewhere else. 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 a hundred and twenty. The selected option carries aria-selected, and an option outside min and max carries aria-disabled so it is still readable rather than skipped. hourLabel, minuteLabel and dayPeriodLabel name the columns.

KeyMoves
One option, wrapping at either end
Home EndFirst or last option of the column
TabThe next column
Enter SpaceSelect the focused option
EscClose 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

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:

AttributeTypeDefaultDescription
minuteStepnumber5Spacing of the minute column. Typed and preset minutes are kept as they are.
hour12booleanfrom localeForces a 12 or 24 hour clock, and with it the AM/PM column.
defaultTimestring'00:00'Time the columns open on while the field is still empty, as HH:mm.
hourLabel / minuteLabel / dayPeriodLabelstring'Hour' / 'Minute' / 'AM or PM'Accessible names of the three columns.

Bindables

AttributeTypeDescription
valuestring | null | undefinedSelected moment as YYYY-MM-DDTHH:mm.
displayMonthstringMonth on screen, as an ISO day. Follows value until navigated, and is restored to what you set when the panel closes.
openbooleanWhether the docked panel is showing.
elementHTMLSpanElementThe 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.

FunctionDescription
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) / formatDateTimeTyped 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 / getDayPeriodLabelsThe clock of a locale, and the labels the time columns are built from.
minutesOfDay / withMinutes / toISOTime / compareTimes / isTimeWithinMinute-precision maths on a moment.