Skip to content

Duration Picker

Form InputsSince 0.3.0
import { DurationPicker } from '@absoluteui/components/duration-picker';
import '@absoluteui/components/duration-picker/style.css';

Using Angular? See Duration Picker for Angular.

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.

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

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) : '';
});
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) : '';
});
}
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) });
<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>
DurationPicker.create(host: HTMLElement, options: DurationPickerOptions): DurationPicker

Global configuration key: durationPicker (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 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.
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.
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.
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).

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).
Slot Default icon
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-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

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 }

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.

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

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

default, segment focused, filled, partly filled, disabled, read-only, required, invalid (message shown).

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
  • 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 native required state.
  • aria-valuemin is always 0; min is enforced by stepping and validation.
  • Manual screen-reader verification is pending (see Known limitations).

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

  • Empty units count as 0 (typing only 3 in 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 at min (or zero) and max (or the digit limit, for example 999:59).
  • Stepping from an empty or invalid entry starts from zero.
  • The step grid starts at zero, not at min. Only the smallest segment moves by step; larger segments always move by one unit, which can leave a step that does not divide that unit (for example 45 minutes) with step-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 max with fewer digits in the largest unit narrows that segment for typing; a programmatic value beyond it (for example 10 hours with max 8 hours) is shown in full and reports range-overflow. Without max, 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 fields through update() keeps the value; switching to a coarser smallest unit (for example from seconds to minutes) rounds the value down to that unit.
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.

  • Negative durations are not supported.
  • Calendar durations (years, months, weeks) are not supported; a day is always 24 hours.
  • The smallest unit is seconds; value and step must be whole numbers of the smallest unit in fields.
  • 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.
  • 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.