Skip to content

Time Picker

Form InputsSince 0.2.0
import { TimePicker } from '@absoluteui/components/time-picker';
import '@absoluteui/components/time-picker/style.css';

Using Angular? See Time Picker for Angular.

Entry of a wall-clock time of day. TimePicker renders locale-ordered segments (hour, minute, optional second, and a day period for 12-hour clocks) inside a labelled group, plus a “Choose time” button that opens a listbox of suggested times at a fixed step between optional min and max bounds. The value is a PlainTime: it has no date and no timezone. Validation is localized and a hidden input submits HH:MM (or HH:MM:SS).

  • A time of day that is not tied to a specific date or timezone: opening hours, a daily reminder, a recurring meeting time.
  • The time part of a composite date-time entry, combined with a date and a zone by consumer code or a date-time picker.
  • Users should be able to both type a time quickly and pick from common times (for example every 30 minutes).
  • An exact moment in time (an instant): use Date Input with minute granularity, which resolves the wall-clock time in an explicit timezone.
  • Durations or elapsed times (for example 1 h 30 min): use Number Input or a dedicated duration field.
  • A handful of fixed slots that should stay visible: use Radio Group or Select.
  • Time ranges that cross midnight in one control: use two pickers and a cross-field rule.
import { plainTime } from '@absoluteui/core';
import { TimePicker } from '@absoluteui/components/time-picker';
const host = document.createElement('div');
document.body.append(host);
const start = TimePicker.create(host, {
label: 'Opening time',
name: 'opensAt',
required: true,
min: plainTime(6, 0),
max: plainTime(22, 0),
step: 15,
});
host.addEventListener('aui-change', () => {
host.dataset.valid = String(start.validity.valid);
});

24-hour entry with seconds and custom validation

Section titled “24-hour entry with seconds and custom validation”
import { parsePlainTime } from '@absoluteui/core';
import { TimePicker, type TimePickerChangeDetail } from '@absoluteui/components/time-picker';
const host = document.createElement('div');
document.body.append(host);
TimePicker.create(host, {
label: 'Backup time',
granularity: 'second',
hourCycle: 'h23',
value: parsePlainTime('02:30:00'),
validate: (value) => (value !== null && value.hour >= 8 && value.hour < 18 ? 'Choose a time outside office hours.' : null),
});
host.addEventListener('aui-change', (event) => {
const detail = (event as CustomEvent<TimePickerChangeDetail>).detail;
host.dataset.time = detail.value ? `${detail.value.hour}:${detail.value.minute}` : '';
});
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { timePicker: { hourCycle: 'h23', step: 15 } } });
import { plainTimeToString } from '@absoluteui/core';
import { TimePicker, type TimePickerChangeDetail } from '@absoluteui/components/time-picker';
import '@absoluteui/components/time-picker/style.css';
const host = document.querySelector<HTMLElement>('#reminder');
if (host) {
TimePicker.create(host, { label: 'Daily reminder', name: 'reminder', step: 15 });
host.addEventListener('aui-change', (event) => {
const { value } = (event as CustomEvent<TimePickerChangeDetail>).detail;
host.dataset.time = value ? plainTimeToString(value) : '';
});
}
import { plainTime } from '@absoluteui/core';
import { TimePicker } from '@absoluteui/components/time-picker';
const host = document.createElement('div');
document.body.append(host);
TimePicker.create(host, { label: 'Opening time', name: 'opensAt', required: true, min: plainTime(6, 0), max: plainTime(22, 0), step: 15 });
<div class="aui-field aui-time-picker" data-aui-control> (consumer host)
<label class="aui-field__label">Label<span class="aui-field__required" aria-hidden="true">*</span></label>
<div class="aui-time-picker__group" role="group" aria-labelledby=label>
<div class="aui-time-picker__fields"> (LTR-isolated run)
<input class="aui-time-picker__segment aui-time-picker__segment--hour" aria-label="Hour">
<span class="aui-time-picker__separator" aria-hidden="true">:</span>
<input class="aui-time-picker__segment aui-time-picker__segment--minute" aria-label="Minute">
<input class="aui-time-picker__segment aui-time-picker__segment--dayPeriod" aria-label="AM/PM"> (12-hour)
</div>
<button class="aui-time-picker__button" aria-haspopup="listbox" aria-expanded aria-controls=listbox>
</div>
<div class="aui-field__description"> (optional)
<div class="aui-field__error aui-validation-message"> (when shown)
<input type="hidden" name>
<div class="aui-time-picker__popup" popover="manual"> (top layer when open)
<div class="aui-time-picker__listbox" role="listbox" tabindex="0" aria-activedescendant>
<div class="aui-time-picker__option" role="option" aria-selected>9:00 AM</div> ...
</div>
<div class="aui-time-picker__empty"> (no suggestion fits)
</div>
</div>
TimePicker.create(host: HTMLElement, options: TimePickerOptions): TimePicker

Global configuration key: timePicker (see Configuration).

Option Type Default Description
label string Visible label and accessible name of the group (via aria-labelledby). Required.
hideLabel boolean false Visually hide the label; it remains the accessible name.
value PlainTime | null null Initial wall-clock time; also the value restored by reset(). Millisecond must be 0; second must be 0 at minute granularity.
min PlainTime | null null Earliest allowed time (inclusive); first suggestion. Must not be after max.
max PlainTime | null null Latest allowed time (inclusive); last possible suggestion.
name string Form field name for the hidden input (HH:MM or HH:MM:SS).
description string Help text referenced by the group’s aria-describedby.
required boolean false An empty entry is invalid; segments are required and the required indicator is shown.
disabled boolean false Disables segments and the button; not submitted.
readOnly boolean false Focusable and submitted; not editable and the listbox does not open.
validate (value: PlainTime | null) => string | null Consumer validation after built-in checks; return a message to mark the value invalid.
icons Partial<{ time: IconRef; error: IconRef }> Icon slot overrides.
validationTiming 'blur' | 'input' | 'submit' "blur" When validation messages first appear; afterwards they update live. Globally configurable.
granularity 'minute' | 'second' "minute" Adds a second segment when ‘second’. Globally configurable. Set at creation only.
hourCycle 'auto' | 'h12' | 'h23' "auto" 12- or 24-hour entry and display; ‘auto’ follows the locale. Globally configurable.
step number 30 Minutes between suggested times (integer 1..720). Does not constrain typed values. Globally configurable.
locale string "en-US" Segment order, hour cycle (auto), day periods, option labels, and messages. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" Listbox alignment direction (applied when the listbox opens). Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
group HTMLDivElement The labelled role=group; control boundary and popup anchor.
button HTMLButtonElement Opens the suggestions listbox.
listbox HTMLDivElement The suggestions listbox.
value PlainTime | null Current value; null when empty, incomplete, or failing a built-in check.
validity TimePickerValidity { valid, error, message }; computed on every change.
isOpen boolean Whether the listbox is shown.
name string | null Form field name.
label string Label text.
disabled boolean Disabled state.
dirty boolean Value differs from the initial value, or a partial entry exists.
touched boolean Focus has left the control at least once.
options Readonly<TimePickerOptions> Current instance options.
Method Description
setValue(value: PlainTime | null): void Programmatic value (no events); clears an external custom error.
update(options: Partial<TimePickerOptions>): void Merge and apply options. Throws CFG_INIT_ONLY when granularity changes.
open(): void Show the listbox and focus it; the option nearest the value (or the first) becomes active.
close(): void Hide the listbox without changing the value; focus inside it returns to the button.
reportValidity(): boolean Show messages now and return validity.
setCustomError(message: string | null): void External error (server, cross-field rule); cleared when the user edits.
getFormValue(): string | null HH:MM (HH:MM:SS at second granularity), or null.
focus(): void Focus the first segment.
reset(): void Close, restore the initial value, and clear dirty/touched/submitted state.
destroy(): void Close, remove rendering, listeners, registration, and subscriptions.
Event Detail Bubbles Cancelable Description
aui-change { value: PlainTime | null; validity: TimePickerValidity } Yes No The user changed the value or its validity (typing, stepping, or choosing a suggestion).
aui-open null No No Listbox opened.
aui-close null No No Listbox closed.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-field aui-time-picker Host.
aui-time-picker--invalid | --disabled | --readonly State modifiers on the host.
aui-field__label | __required | __description | __error Shared field chrome.
aui-time-picker__group Labelled group; control boundary and popup anchor.
aui-time-picker__fields LTR-isolated run of segments and separators.
aui-time-picker__segment | __segment--hour | --minute | --second | --dayPeriod Segment inputs.
aui-time-picker__separator Locale separator text (aria-hidden).
aui-time-picker__button Choose time button.
aui-time-picker__popup Top-layer popover container.
aui-time-picker__listbox Suggestions listbox (focusable).
aui-time-picker__option Suggested time; –active marks the active descendant.
aui-time-picker__empty Shown when no suggestion fits between min and max.
Slot Default icon
time clock
error alert-circle

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-height-sm | --aui-control-padding-x | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-popover-max-height | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-3

type TimePickerGranularity = 'minute' | 'second';
type TimePickerHourCycle = 'auto' | 'h12' | 'h23';
type TimePickerError = 'incomplete' | 'invalid' | 'value-missing' | 'range-underflow' | 'range-overflow' | 'custom';
interface TimePickerValidity { readonly valid: boolean; readonly error: TimePickerError | null; readonly message: string }
interface TimePickerChangeDetail { value: PlainTime | null; validity: TimePickerValidity }
interface TimePickerIcons { time: IconRef; error: IconRef }

Pattern: Segmented time field in a labelled group (as Date Input) with a button that opens a WAI-ARIA APG listbox popup: focus moves into the single-select listbox, which conveys the active option through aria-activedescendant.

Key Context Action
Digits Hour, minute, or second segment Enters digits; advances when the segment is full or no further digit could be valid (for example 7 in the hour of a 24-hour clock).
A / P (or the localized day period) Day-period segment Sets AM or PM and advances.
: . , - / Space Numeric segment Pads a single digit and advances to the next segment.
ArrowUp / ArrowDown Segment Increments or decrements with wrap (hours 1-12 or 0-23, minutes and seconds 0-59); toggles AM/PM. Empty segments start at the minimum (Up) or maximum (Down).
ArrowLeft / ArrowRight Caret at the segment edge Previous or next segment, following the visual order of the LTR-isolated segment run.
Backspace Empty segment Moves to the previous segment.
Alt+ArrowDown Segment Opens the listbox.
Enter / Space Button Opens the listbox (or closes it when open) and moves focus into it.
ArrowDown / ArrowUp Listbox Moves the active option without wrapping.
Home / End Listbox First / last option.
PageDown / PageUp Listbox Moves the active option by 10.
Printable characters Listbox Typeahead: activates the next option whose label starts with the typed text; Space continues a query in progress.
Enter / Space Listbox Selects the active option, closes, returns focus to the first segment, emits aui-change if the value changed.
Escape Listbox Closes without changing the value and returns focus to the button; does not reach a surrounding Dialog.
Tab / Shift+Tab Listbox Closes without selecting and moves focus.
Tab / Shift+Tab Group Moves through the segments, then the button.
Element Role
segment container group
each segment (input type=text) textbox (native)
choose time button button (native)
suggestions container (tabindex=0) listbox
each suggestion option
separators and icons none (aria-hidden=true)
Attribute Element Value
aria-labelledby group Label id
aria-describedby group Help text and visible error message ids
aria-label segment Localized segment name (Hour, Minute, Second, AM/PM)
aria-invalid segment “true” only while an error message is shown
aria-label button Localized “Choose time”
aria-haspopup button listbox
aria-expanded button true when the listbox is shown
aria-controls button listbox id
aria-labelledby listbox Label id
aria-activedescendant listbox id of the active option while open; removed when closed
aria-selected option “true” for the option equal to the value, “false” otherwise

Each segment and the button are tab stops. Opening the listbox moves DOM focus to the listbox (tabindex=0); the active option is conveyed through aria-activedescendant, has a visible outline that does not rely on color alone, and is scrolled into view (centered on open). Selecting returns focus to the first segment; Escape and closing through the button return it to the button; focus is moved before the popup hides so it never falls to the document body.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
2.5.3 Label in Name A
3.2.2 On Input A
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • The label is a <label> associated through aria-labelledby because a group is not labelable; clicking the label focuses the first segment.
  • The required asterisk is aria-hidden; each segment carries the native required state.
  • Errors appear after the first blur of the whole control by default, so moving between segments does not trigger messages.
  • The button name is constant (“Choose time”); aria-expanded conveys the popup state.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver, TalkBack) is pending.
  • Segment arrow navigation follows visual order and is not mirrored in RTL, unlike Date Input, because segments render as an LTR run.

default, segment focused, filled, incomplete, open, option active, option selected, disabled, read-only, required, invalid (message shown).

From Trigger To
empty user types part of a time dirty, incomplete; message shown only with validationTiming: 'input'
incomplete last segment filled valid, or invalid / range-underflow / range-overflow; aui-change
closed button, Alt+ArrowDown, or open() open; focus in the listbox; aui-open
open Enter, Space, or click on an option closed; value set; focus on the first segment; aui-close, then aui-change if changed
open Escape or button closed; value kept; focus on the button
open Tab, Shift+Tab, or a click outside closed; focus moves on
any focus leaves the host touched; messages shown (blur timing)
any reportValidity() (for example on form submit) submitted; messages shown for every timing
invalid (custom error) user edits custom error cleared, validity recomputed
any reset() initial value; pristine, untouched, closed
  • DOM focus moves into the listbox on open (APG listbox popup). The active option has a visible outline that does not rely on color alone and is centered into view on open.
  • Focus is moved before the popup hides, so it never falls to the document body.
  • Moving between segments does not count as leaving the field; errors appear after the whole control loses focus.
  • The required asterisk is aria-hidden; each segment carries the native required state.
  • Manual screen-reader verification is pending (see Known limitations).

The group hugs its segments and button, up to 100% of the container; segments wrap inside the group in very narrow containers. The listbox is at least as wide as the group, flips above when space below is insufficient, and caps its height to the available space.

Segment order comes from the locale’s Intl pattern. Segments render as an LTR-isolated run in every direction, because times read left to right; ArrowLeft and ArrowRight therefore follow that visual order and are not mirrored (Date Input mirrors them). The button sits at the inline end, and the listbox aligns to the inline start of the group using direction.

Colors come from role tokens only. The focused segment uses the color-selected / color-selected-text pair. In forced-colors mode the focused segment and the selected option use Highlight / HighlightText, and the active option outline uses Highlight.

configureAbsoluteUI({ components: { timePicker: { hourCycle: 'h23', step: 15, validationTiming: 'submit' } } });

Dynamic options apply to existing instances immediately; a locale or hour-cycle change rebuilds the segments and keeps the value.

Instance options override global and theme defaults. granularity is init-only; all other options are dynamic.

Not timezone-bound. The value is a PlainTime: a wall-clock time of day with no date and no zone. Formatting uses only the locale and hour cycle, and the component never reads the host machine’s timezone, so the same value shows the same time everywhere. Daylight-saving gaps and overlaps do not exist for a PlainTime; they only appear once a date and a zone are attached. Combining a time with a date and a timezone (and applying the DST disambiguation policy) is the job of a DateTime Picker or consumer code, for example toInstant(plainDateTime(date, time), zone, disambiguation).

icons.time (default clock) for the button and icons.error (default alert-circle) for the validation message.

All text uses textContent. Validation messages never include the entered value; range messages include only the configured bounds. CSP-safe: no inline styles or HTML; positioning writes styles through the CSSOM.

No persistence, network access, or telemetry. When name is set, a hidden input carries HH:MM (HH:MM:SS at second granularity); it is empty while the entry is empty or invalid, and disabled (not submitted) while the picker is disabled.

Suggestions are built only when the listbox opens. Listeners are delegated: segment keyboard and input handling share one listener each on the segment container. One configuration subscription per instance. Budget: open() with step: 1 (1,440 suggestions) under 100 ms in headless Chromium.

Up to 1,440 suggestions (step 1 over a whole day), covered by the unit test.

  • 12:xx AM is 00:xx, and 12:xx PM is 12:xx.
  • Typing 29 in a 24-hour hour, or 00 or 13 in a 12-hour hour, reports invalid.
  • min: 09:14:30 at minute granularity starts the suggestions at 09:15, so a chosen suggestion is never below min.
  • If no suggestion fits between min and max, the listbox shows “No times available”.
  • Typed values do not have to align with step; the step only shapes the suggestions.
  • When there is no value, the first suggestion is active on open; the current clock time is not used, because the value has no zone.
Code Category Cause
TIME_PICKER_OPTIONS configuration Missing label, min after max, a bound or value that is not a PlainTime, a value with seconds at minute granularity or with milliseconds, or a non-function validate.
CFG_INVALID_VALUE configuration Invalid step, hourCycle, granularity, validationTiming, locale, or direction (reported by the configuration engine).
CFG_INIT_ONLY configuration update() changes granularity.
Validation (validity.error) validation incomplete, invalid, value-missing, range-underflow, range-overflow, custom.

A failed construction leaves no rendering, listeners, or subscriptions behind.

  • Ranges that cross midnight (min after max) are not supported.
  • Only ASCII digits are accepted in segments.
  • Day periods whose localized forms share a first character (for example Korean 오전/오후) need the full text, Latin a/p, or ArrowUp/ArrowDown.
  • Suggestions are not virtualized (maximum 1,440).
  • Milliseconds are not supported.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, VoiceOver, and TalkBack.
  • HTML in the label, help text, options, or messages.
  • Timezone conversion or DST handling (the value has no zone).
  • Custom suggestion lists or individually disabled suggestions.