Duration Field
Accessible, unstyled segmented duration input that reads and writes ISO 8601 durations.
| Package | @vizejs/ui/duration-field |
| Maturity | stable |
| Own the source | vize lib pull duration-field |
| Requires | controllable-state, date-field, id, locale |
| Aliases | duration input, timespan, iso 8601 duration, estimate field |
| Covers | ISO 8601 durations, Temporal.Duration, WAI-ARIA spinbutton |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { DurationField } from "@vizejs/ui/duration-field";
Or copy the source into your project with vize lib pull duration-field (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Duration field editing hours and minutes and submitting an ISO 8601 duration. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import { DurationField, formatIsoDuration } from "@vizejs/ui/duration-field";
import type { DurationValue } from "@vizejs/ui/duration-field";
const estimate = ref<DurationValue | null>({ hours: 1, minutes: 30 });
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Estimated effort</span>
<DurationField
v-model="estimate"
name="estimate"
locale="en-US"
:fields="['hours', 'minutes']"
unit-display="short"
:aria-labelledby="labelId"
/>
<output>{{ estimate ? formatIsoDuration(estimate) : "Not set" }}</output>
</div>
</template>
API
DurationField
Source: src/families/date-time/duration-field/duration-field.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null | undefined |
undefined |
Consumer-owned group id; nullish values use a deterministic fallback. |
name |
string | undefined |
undefined |
Input name that submits the ISO 8601 duration (for example PT1H30M). |
modelValue |
DurationValue | null | undefined |
undefined |
Controlled duration; undefined selects uncontrolled mode and null clears. |
defaultValue |
DurationValue | null | undefined |
null |
Initial uncontrolled duration and native form-reset target. |
fields |
readonly DurationUnit[] | undefined |
["hours", "minutes"] |
Units rendered as segments, always shown largest first. |
unitDisplay |
DurationUnitDisplay | undefined |
"short" |
Unit label width next to each segment. |
maxValue |
number | undefined |
9999 |
Upper bound of units that do not roll into a larger edited unit. |
locale |
string | undefined |
undefined |
BCP 47 locale for unit labels; defaults to the nearest LocaleProvider. |
dir |
"ltr" | "rtl" | undefined |
undefined |
Text direction for arrow-key segment movement. |
disabled |
boolean | undefined |
false |
Disable every segment. |
readOnly |
boolean | undefined |
false |
Keep segments focusable while blocking edits. |
required |
boolean | undefined |
false |
Require a complete value; participates in native form validation. |
placeholder |
string | undefined |
"––" |
Placeholder shown in empty segments. |
emptyText |
string | undefined |
"Empty" |
Text announced for empty segments. |
ariaLabel |
string | undefined |
undefined |
Accessible name for the segment group. |
ariaLabelledby |
string | undefined |
undefined |
Ids that label the segment group. |
ariaDescribedby |
string | undefined |
undefined |
Ids that describe the group and every segment. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: DurationValue | null] |
Fired when segments request a new controlled duration or null while incomplete. |
change |
[value: DurationValue | null, previous: DurationValue | null, nativeEvent: Event | null] |
Fired after a distinct committed duration with the previous value and triggering event. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
DurationFieldSlotState |
Extra content after the segments. Receives field state. |
segment |
DurationSegmentState |
Segment amount content. Receives the segment state; defaults to its text. |
Behavior
Normative state x input -> outcome table for duration-field.vue
(@vizejs/ui/duration-field): segmented spinbuttons for whole-unit durations
that read and write typed ISO 8601 durations (IsoDuration = `P${string}`).
Every row is proven by the named test.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| DU1 | any | render | fields render largest first as role="spinbutton" segments with localized unit affixes (Intl.NumberFormat units) and aria-valuetext like 2 hours; units that roll into an edited larger unit are bounded (hours 0–23 under days, minutes/seconds 0–59, months 0–11 under years, days 0–6 under weeks); name submits the ISO duration |
renders unit segments with localized affixes, bounds, and an ISO input |
| DU2 | editing | digits / arrows / Page / Backspace / Delete / Left / Right | digits fill and auto-advance; arrows wrap; Page steps by 10; Backspace drops a digit; Delete clears; the value commits (zeros dropped) only when every unit is filled | typing and stepping fill units and commit normalized durations |
| DU3 | required / read-only |
form validation / keys | required fields join native constraint validation; read-only fields ignore edits | required durations join native validation and read-only blocks edits |
| DU4 | imperative | expose | focus(unit?), setValue, clear |
exposes focus, setValue, and clear |
| DU5 | model | helpers | formatIsoDuration/parseIsoDuration round-trip whole units (fractions and signs are rejected); durationToSeconds refuses calendar units |
ISO 8601 durations round-trip and convert to seconds |
| DU6 | SSR | isolated requests | byte-identical markup and silent hydration | renders byte-identical duration field markup across isolated SSR requests, hydrates duration field markup without mismatches |