Time Picker
import { TimePicker } from '@absoluteui/components/time-picker';import '@absoluteui/components/time-picker/style.css';Using Angular? See Time Picker for Angular.
What it does
Section titled “What it does”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).
When to use
Section titled “When to use”- 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).
When not to use
Section titled “When not to use”- 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.
Examples
Section titled “Examples”Required time with business-hours bounds
Section titled “Required time with business-hours bounds”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}` : '';});Global hour cycle and suggestion step
Section titled “Global hour cycle and suggestion step”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) : ''; });}More usage
Section titled “More usage”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 });Anatomy
Section titled “Anatomy”<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>API reference
Section titled “API reference”Factory
Section titled “Factory”TimePicker.create(host: HTMLElement, options: TimePickerOptions): TimePickerGlobal configuration key: timePicker (see Configuration).
Options
Section titled “Options”| 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. |
Properties
Section titled “Properties”| 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. |
Methods
Section titled “Methods”| 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. |
Events
Section titled “Events”| 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. |
CSS classes
Section titled “CSS classes”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. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
time |
clock |
error |
alert-circle |
Design tokens
Section titled “Design tokens”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
TypeScript definitions
Section titled “TypeScript definitions”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 }Accessibility
Section titled “Accessibility”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.
Keyboard
Section titled “Keyboard”| 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) |
ARIA attributes
Section titled “ARIA attributes”| 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.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- 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.
Behavior
Section titled “Behavior”States
Section titled “States”default, segment focused, filled, incomplete, open, option active, option selected, disabled, read-only, required, invalid (message shown).
State transitions
Section titled “State transitions”| 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 |
Accessibility notes
Section titled “Accessibility notes”- 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 nativerequiredstate. - Manual screen-reader verification is pending (see Known limitations).
Responsive behavior
Section titled “Responsive behavior”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.
Right-to-left
Section titled “Right-to-left”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.
Theming
Section titled “Theming”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.
Global configuration
Section titled “Global configuration”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.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. granularity is init-only; all other options are dynamic.
Timezones
Section titled “Timezones”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.
Security
Section titled “Security”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.
Data handling
Section titled “Data handling”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.
Performance
Section titled “Performance”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.
Supported data scale
Section titled “Supported data scale”Up to 1,440 suggestions (step 1 over a whole day), covered by the unit test.
Edge cases
Section titled “Edge cases”12:xx AMis00:xx, and12:xx PMis12:xx.- Typing
29in a 24-hour hour, or00or13in a 12-hour hour, reportsinvalid. min: 09:14:30at minute granularity starts the suggestions at 09:15, so a chosen suggestion is never belowmin.- If no suggestion fits between
minandmax, 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.
Errors
Section titled “Errors”| 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.
Known limitations
Section titled “Known limitations”- Ranges that cross midnight (
minaftermax) 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.
Unsupported scenarios
Section titled “Unsupported scenarios”- 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.