Duration Picker
import { DurationPicker } from '@absoluteui/components/duration-picker';import '@absoluteui/components/duration-picker/style.css';Using Angular? See Duration Picker for Angular.
What it does
Section titled “What it does”Entry of a fixed-length amount of time. DurationPicker renders one spinbutton segment per unit (days, hours, minutes, seconds; configurable and contiguous, default hours and minutes) inside a labelled group, each with localized unit text. ArrowUp and ArrowDown change the total by one unit and carry into the neighbouring units; the smallest unit moves by step. The value is a Duration: a non-negative number of milliseconds with no date, clock position, or timezone. Validation is localized and a hidden input submits an ISO 8601 duration (PT1H30M). There is no popup.
When to use
Section titled “When to use”- An amount of elapsed or planned time: a task estimate, a timeout, a meeting length, a lap or cooking time.
- Values that users think of in several units at once (1 h 30 min) rather than as one number of minutes.
- Durations that must stay on a grid (for example quarter hours) or within bounds.
When not to use
Section titled “When not to use”- A time of day (09:30): use Time Picker; a duration has no clock position.
- Calendar periods such as months or years, whose length depends on the start date: not supported; use Number Input with a unit, or a date range.
- Negative durations or offsets (for example -5 min): not supported.
- A single unit with a large range (for example 0-10,000 seconds): Number Input is simpler.
- Sub-second precision: the smallest unit is seconds.
Examples
Section titled “Examples”Required estimate in quarter hours, up to 8 hours
Section titled “Required estimate in quarter hours, up to 8 hours”import { duration } from '@absoluteui/core';import { DurationPicker } from '@absoluteui/components/duration-picker';
const host = document.createElement('div');document.body.append(host);const estimate = DurationPicker.create(host, { label: 'Estimate', name: 'estimate', required: true, min: duration(15 * 60_000), max: duration(8 * 3_600_000), step: duration(15 * 60_000),});host.addEventListener('aui-change', () => { host.dataset.valid = String(estimate.validity.valid);});Minutes and seconds from an ISO 8601 value, with custom validation
Section titled “Minutes and seconds from an ISO 8601 value, with custom validation”import { durationToString, parseDuration } from '@absoluteui/core';import { DurationPicker, type DurationPickerChangeDetail } from '@absoluteui/components/duration-picker';
const host = document.createElement('div');document.body.append(host);DurationPicker.create(host, { label: 'Lap time', fields: ['minutes', 'seconds'], value: parseDuration('PT1M30S'), validate: (value) => (value !== null && value.milliseconds === 0 ? 'Enter a lap time above zero.' : null),});host.addEventListener('aui-change', (event) => { const { value } = (event as CustomEvent<DurationPickerChangeDetail>).detail; host.dataset.lap = value ? durationToString(value) : '';});Global units and validation timing
Section titled “Global units and validation timing”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { durationPicker: { fields: ['days', 'hours', 'minutes'], validationTiming: 'input' } } });import { durationToString } from '@absoluteui/core';import { DurationPicker, type DurationPickerChangeDetail } from '@absoluteui/components/duration-picker';import '@absoluteui/components/duration-picker/style.css';
const host = document.querySelector<HTMLElement>('#timeout');if (host) { DurationPicker.create(host, { label: 'Session timeout', name: 'timeout', fields: ['minutes', 'seconds'] }); host.addEventListener('aui-change', (event) => { const { value } = (event as CustomEvent<DurationPickerChangeDetail>).detail; host.dataset.timeout = value ? durationToString(value) : ''; });}More usage
Section titled “More usage”import { duration } from '@absoluteui/core';import { DurationPicker } from '@absoluteui/components/duration-picker';
const host = document.createElement('div');document.body.append(host);DurationPicker.create(host, { label: 'Estimate', name: 'estimate', required: true, max: duration(8 * 3_600_000), step: duration(15 * 60_000) });Anatomy
Section titled “Anatomy”<div class="aui-field aui-duration-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-duration-picker__group" role="group" aria-labelledby=label dir> <span class="aui-duration-picker__unit aui-duration-picker__unit--hours"> <input class="aui-duration-picker__segment aui-duration-picker__segment--hours" role="spinbutton" aria-label="Hours" aria-valuenow aria-valuetext="1 hour" aria-valuemin="0" aria-valuemax="999"> <span class="aui-duration-picker__unit-label" aria-hidden="true">hr</span> (before the input where the locale puts units first) </span> <span class="aui-duration-picker__unit aui-duration-picker__unit--minutes"> ... </span> </div> <div class="aui-field__description"> (optional) <div class="aui-field__error aui-validation-message"> (when shown) <input type="hidden" name></div>API reference
Section titled “API reference”Factory
Section titled “Factory”DurationPicker.create(host: HTMLElement, options: DurationPickerOptions): DurationPickerGlobal configuration key: durationPicker (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 |
Duration | null |
null |
Initial duration; also the value restored by reset(). Non-negative and a whole number of the smallest unit in fields. |
min |
Duration | null |
null |
Inclusive lower bound for validation and stepping (non-negative). Stepping stops at zero when unset. Must not exceed max. |
max |
Duration | null |
null |
Inclusive upper bound for validation and stepping (non-negative); also sets the digits and aria-valuemax of the largest segment. |
step |
Duration |
"one of the smallest unit" |
Increment of the smallest segment and the valid-value grid from zero. Positive and a whole number of the smallest unit. |
name |
string |
Form field name for the hidden input (ISO 8601 duration, for example PT1H30M). | |
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 the segments; not submitted. |
readOnly |
boolean |
false |
Focusable and submitted; not editable, and stepping, Home/End, and separators are ignored. |
validate |
(value: Duration | null) => string | null |
Consumer validation after built-in checks; return a message to mark the value invalid. | |
icons |
Partial<{ error: IconRef }> |
Icon slot overrides. | |
validationTiming |
'blur' | 'input' | 'submit' |
"blur" |
When validation messages first appear; afterwards they update live. Globally configurable. |
fields |
readonly DurationField[] |
["hours","minutes"] |
Unit segments, largest first, as a contiguous non-empty run of ‘days’ | ‘hours’ | ‘minutes’ | ‘seconds’. Globally configurable. |
locale |
string |
"en-US" |
Segment names, unit text and its position, aria-valuetext, durations in messages, and messages. Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
Direction of the group; ArrowLeft/ArrowRight follow it. 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 with one spinbutton segment per unit. |
value |
Duration | null |
Current value; null when empty or failing a built-in check (a custom error from validate or setCustomError keeps the value, as in Time Picker). |
validity |
DurationPickerValidity |
{ valid, error, message }; computed on every change. |
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 or invalid entry exists. |
touched |
boolean |
Focus has left the control at least once. |
options |
Readonly<DurationPickerOptions> |
Current instance options. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
setValue(value: Duration | null): void |
Programmatic value (no events); clears an external custom error. Throws DURATION_PICKER_OPTIONS for a negative value or one that is not a whole number of the smallest unit. |
update(options: Partial<DurationPickerOptions>): void |
Merge and apply options; a fields, locale, or max change rebuilds the segments and keeps the current value. |
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 |
ISO 8601 duration in hours, minutes, and seconds (PT1H30M), or null when empty or invalid. |
focus(): void |
Focus the first (largest) segment. |
reset(): void |
Restore the initial value and clear dirty/touched/submitted state (no events). |
destroy(): void |
Remove rendering, listeners, form registration, and subscriptions. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-change |
{ value: Duration | null; validity: DurationPickerValidity } |
Yes | No | The user changed the value or its validity error (typing, stepping, Home/End, or a separator). |
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-duration-picker |
Host. |
aui-duration-picker--invalid | --disabled | --readonly |
State modifiers on the host. |
aui-field__label | __required | __description | __error |
Shared field chrome. |
aui-duration-picker__group |
Labelled group; control boundary and focus indicator. |
aui-duration-picker__unit | __unit--days | --hours | --minutes | --seconds |
Wrapper of one segment and its unit text. |
aui-duration-picker__segment | __segment--days | --hours | --minutes | --seconds |
Spinbutton segment inputs. |
aui-duration-picker__unit-label |
Short localized unit text (aria-hidden). |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
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-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-padding-x | --aui-duration-fast | --aui-easing-standard | --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-radius-md | --aui-radius-sm | --aui-space-1 | --aui-space-2
TypeScript definitions
Section titled “TypeScript definitions”type DurationField = 'days' | 'hours' | 'minutes' | 'seconds';type DurationPickerError = 'invalid' | 'value-missing' | 'range-underflow' | 'range-overflow' | 'step-mismatch' | 'custom';interface DurationPickerConfig { validationTiming: ValidationTiming; fields: readonly DurationField[] }interface DurationPickerValidity { readonly valid: boolean; readonly error: DurationPickerError | null; readonly message: string }interface DurationPickerChangeDetail { value: Duration | null; validity: DurationPickerValidity }interface DurationPickerIcons { error: IconRef }Accessibility
Section titled “Accessibility”Pattern: Labelled group of WAI-ARIA APG Spinbutton segments (one text input per unit) with no popup; each segment exposes its unit value and a localized value text.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Digits | Segment | Enters digits (other characters are removed); advances when the segment is full, or, below the largest unit, when no second digit could be valid (for example 7 minutes), padding to two digits. |
| : . , Space | Segment | Pads a single digit below the largest unit and advances to the next segment. |
| ArrowUp / ArrowDown | Segment | Changes the total by one unit of the segment and carries into the other units; the smallest segment moves by step, snapping to the step grid first. Clamped to [min or 0, max or the digit limit]; an empty entry starts from zero. |
| Home / End | Segment | Sets the segment to 0 / its aria-valuemax. |
| ArrowLeft / ArrowRight | Caret at the segment edge, or text selected | Previous or next unit in reading order (mirrored in RTL). |
| Backspace | Empty segment | Moves to the previous segment. |
| Tab / Shift+Tab | Group | Moves through the segments, then out of the control. |
| Element | Role |
|---|---|
| segment container | group |
| each segment (input type=text) | spinbutton |
| unit text | none (aria-hidden=true) |
| error icon | 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 unit name (Days, Hours, Minutes, Seconds) |
aria-valuenow |
segment | Number in the segment; removed when empty |
aria-valuetext |
segment | Number with the long localized unit (for example “30 minutes”); removed when empty |
aria-valuemin |
segment | 0 |
aria-valuemax |
segment | 23 for hours and 59 for minutes and seconds below a larger unit; for the largest segment, max in that unit (rounded down) or the digit limit (for example 999) |
aria-invalid |
segment | “true” only while an error message is shown |
Each segment is a tab stop; focusing one selects its text. The group shows the focus indicator (border in color-focus-ring plus a 1px ring) while any segment is focused, and the focused segment uses the selected colors with a focus-ring outline on :focus-visible. Clicking the label focuses the first segment. Rebuilding the segments (fields, locale, or max change) keeps focus on the same unit.
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 it focuses the first segment unless disabled.
- The visible unit text is aria-hidden because each segment’s name and aria-valuetext already carry the unit.
- 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.
- Validation messages format durations in the locale with short units (for example ‘Enter a duration of 1 hr, 30 min or more.’).
- Screen-reader behavior (spinbutton names, aria-valuetext on step) has not yet been manually verified with NVDA, JAWS, and VoiceOver.
- aria-valuemin is always 0 per segment; min is enforced by stepping and validation, not exposed on the segments.
Behavior
Section titled “Behavior”States
Section titled “States”default, segment focused, filled, partly filled, disabled, read-only, required, invalid (message shown).
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| empty | user types in a segment | dirty; value from the filled units (empty units count as 0); aui-change |
| partly filled | focus leaves the host with a valid value | empty units filled with zeros |
| valid | a segment above its range (for example 75 minutes), or a bound or step violated | invalid, range-underflow, range-overflow, or step-mismatch; value null; aui-change |
| any | ArrowUp / ArrowDown | total stepped and clamped; aui-change if the value or error changed |
| 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 |
Accessibility notes
Section titled “Accessibility notes”- Focusing a segment selects its text, so typing replaces it.
- Moving between segments does not count as leaving the field; errors appear after the whole control loses focus. Rendering the message is deferred while a pointer is pressed, so it does not shift the pending click target.
- Clicking the label focuses the first segment (unless disabled).
- The required asterisk is
aria-hidden; each segment carries the nativerequiredstate. aria-valueminis always 0;minis enforced by stepping and validation.- Manual screen-reader verification is pending (see Known limitations).
Responsive behavior
Section titled “Responsive behavior”The group hugs its units up to 100% of the container; units wrap inside the group in very narrow containers. Segments are sized in ch with tabular digits; the largest segment is wider for extra digits.
Right-to-left
Section titled “Right-to-left”The group takes dir from direction, so units flow right to left in RTL. ArrowLeft and ArrowRight follow the reading direction and are mirrored in RTL. Unit text is placed before or after the number as the locale’s Intl.NumberFormat unit formatting does.
Theming
Section titled “Theming”Colors come from role tokens only. The group border and a 1px ring use color-focus-ring while a segment is focused; the focused segment uses the color-selected / color-selected-text pair. In forced-colors mode the focused group is outlined in Highlight, the focused segment uses Highlight / HighlightText, and disabled unit text uses GrayText.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { durationPicker: { fields: ['days', 'hours', 'minutes'], validationTiming: 'submit' } } });Dynamic options apply to existing instances immediately; a fields or locale change rebuilds the segments and keeps the current value.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic; update() rebuilds the segments when fields, locale, or max changes and keeps the current value.
Timezones
Section titled “Timezones”Not applicable. A Duration is a fixed number of milliseconds with no date, clock position, or timezone, and the component does not inherit the timezone option. A day is exactly 24 hours, so daylight-saving transitions never change a value. Adding a duration to a date in a zone (where a calendar day may be 23 or 25 hours) is consumer code.
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 and step messages include only the configured bounds and step. CSP-safe: no inline styles or HTML.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. When name is set, a hidden input carries the ISO 8601 duration in hours, minutes, and seconds (PT1H30M, PT0S for zero). Days are never emitted: a day and two hours is PT26H. The input is empty while the entry is empty or invalid, and disabled (not submitted) while the picker is disabled. parseDuration from @absoluteui/core reads these strings (and P1DT2H) back into a Duration.
Performance
Section titled “Performance”No popup and no per-segment listeners: keyboard, input, and focus handling are delegated to one listener each on the group. Segments are rebuilt only when fields, locale, or max changes. One configuration subscription per instance. Budget: DurationPicker.create under 2 ms average per instance (100 instances) in headless Chromium.
Supported data scale
Section titled “Supported data scale”Up to four unit segments. The largest segment holds as many digits as max needs in that unit; without max, 3 digits for days and hours, 4 for minutes, and 5 for seconds (999 hours, 9,999 minutes).
Edge cases
Section titled “Edge cases”- Empty units count as 0 (typing only
3in hours is 3 hours); they are filled with zeros when focus leaves the control. - A unit below the largest that exceeds its range (75 minutes, 24 hours) reports
invalid; it is not normalized. - Stepping carries across units (
0:59+ 1 minute =1:00) and stops atmin(or zero) andmax(or the digit limit, for example999:59). - Stepping from an empty or
invalidentry starts from zero. - The step grid starts at zero, not at
min. Only the smallest segment moves bystep; larger segments always move by one unit, which can leave a step that does not divide that unit (for example 45 minutes) withstep-mismatch. - Home and End set only the focused segment (0 or its
aria-valuemax), so they can produce a range or step error that validation then reports. - A
maxwith fewer digits in the largest unit narrows that segment for typing; a programmatic value beyond it (for example 10 hours withmax8 hours) is shown in full and reportsrange-overflow. Withoutmax, a programmatic value may exceed the digit limit of the largest segment (for example 1,000 hours); it is valid, but stepping stops at the digit limit. - Durations in messages use the units in
fields(Enter a duration of 1 hr, 30 min or more.), adding seconds when a bound is not a whole number of the smallest unit. - Changing
fieldsthroughupdate()keeps the value; switching to a coarser smallest unit (for example from seconds to minutes) rounds the value down to that unit.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
DURATION_PICKER_OPTIONS |
configuration | Missing label; a value that is not a non-negative Duration in whole smallest units; a min or max that is not a non-negative Duration; min after max; a step that is not a positive Duration in whole smallest units; a non-function validate. |
CFG_INVALID_VALUE |
configuration | Invalid fields (empty, non-contiguous, or not largest first), validationTiming, locale, or direction (reported by the configuration engine). |
Validation (validity.error) |
validation | invalid, value-missing, range-underflow, range-overflow, step-mismatch, custom. |
Precedence: the built-in check (value-missing for an empty required entry, then invalid, range-underflow, range-overflow, step-mismatch), then validate, then an external custom error. English messages: “This field is required.”, “Enter a valid duration.”, “Enter a duration of {min} or more.”, “Enter a duration of {max} or less.”, “Enter a duration in steps of {step}.” Durations in messages use short localized units joined by Intl.ListFormat (1 hr, 30 min).
A failed construction leaves no rendering, listeners, or subscriptions behind.
Known limitations
Section titled “Known limitations”- Negative durations are not supported.
- Calendar durations (years, months, weeks) are not supported; a day is always 24 hours.
- The smallest unit is seconds;
valueandstepmust be whole numbers of the smallest unit infields. - Without
max, the largest segment is limited by its digits. - Only ASCII digits are accepted in segments.
- No popup or preset list.
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- HTML in the label, help text, or messages.
- Non-contiguous fields (for example days and minutes without hours).
- Negative or calendar durations; day, week, month, or year designators in the form value.