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.

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

<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.

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

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:

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.