Time Field
Accessible, unstyled segmented time input with 12/24-hour clocks and hour/minute/second granularity.
| Package | @vizejs/ui/time-field |
| Maturity | stable |
| Own the source | vize lib pull time-field |
| Requires | controllable-state, date-field, id, locale |
| Aliases | time input, segmented time, clock input, hh:mm |
| Covers | WAI-ARIA spinbutton, React Aria TimeField, Reka UI TimeField, HTML input type=time |
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 { TimeField } from "@vizejs/ui/time-field";
Or copy the source into your project with vize lib pull time-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
<!-- Controlled segmented time field with a fixed locale, business-hour bounds, and ISO output. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import { createPlainTime, formatIsoTime, TimeField } from "@vizejs/ui/time-field";
import type { PlainTime } from "@vizejs/ui/time-field";
const start = ref<PlainTime | null>(createPlainTime(9, 30));
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Meeting start</span>
<TimeField
v-model="start"
name="start"
locale="en-US"
:min="createPlainTime(8)"
:max="createPlainTime(18)"
:aria-labelledby="labelId"
/>
<output>{{ start ? formatIsoTime(start) : "No time selected" }}</output>
</div>
</template>
API
TimeField
Source: src/families/date-time/time-field/time-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 |
Hidden input name that submits HH:MM or HH:MM:SS with forms. |
modelValue |
PlainTime | null | undefined |
undefined |
Controlled time; undefined selects uncontrolled mode and null clears. |
defaultValue |
PlainTime | null | undefined |
null |
Initial uncontrolled time and native form-reset target. |
min |
PlainTime | null | undefined |
undefined |
Earliest valid time; earlier values mark the field invalid. |
max |
PlainTime | null | undefined |
undefined |
Latest valid time; later values mark the field invalid. |
hourCycle |
HourCycle | undefined |
undefined |
12 shows 1–12 plus an AM/PM segment, 24 shows 0–23; defaults to the locale clock. |
granularity |
TimeGranularity | undefined |
"minute" |
Smallest edited unit; second adds a seconds segment. |
placeholderValue |
PlainTime | null | undefined |
{ hour: 0, minute: 0, second: 0 } |
Time whose segments seed arrow-key stepping from empty. |
locale |
string | undefined |
undefined |
BCP 47 locale deciding segment order, clock, and labels; defaults to the nearest LocaleProvider. |
dir |
"ltr" | "rtl" | undefined |
undefined |
Text direction for arrow-key segment movement; defaults to the nearest LocaleProvider. |
disabled |
boolean | undefined |
false |
Remove every segment from focus and editing. |
readOnly |
boolean | undefined |
false |
Keep segments focusable while blocking edits. |
required |
boolean | undefined |
false |
Require a complete value; participates in native form validation. |
invalidMessage |
string | undefined |
"Invalid value" |
Custom validity message reported while the value is out of range or unavailable. |
placeholders |
FieldSegmentPlaceholders | undefined |
{ hour: "––", minute: "––", second: "––", dayPeriod: "––" } |
Placeholder text for 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. |
ariaErrormessage |
string | undefined |
undefined |
Id of the validation message used while invalid. |
ariaInvalid |
boolean | undefined |
false |
Force the invalid state in addition to built-in min/max validation. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: PlainTime | null] |
Fired when segments request a new controlled time or null while incomplete. |
change |
[value: PlainTime | null, previous: PlainTime | null, nativeEvent: Event | null] |
Fired after a distinct committed time with the previous time and triggering event. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
TimeFieldSlotState |
Extra content after the segments, such as a picker trigger. Receives field state. |
segment |
FieldSegmentState |
Segment content. Receives the segment state; defaults to its text. |
Behavior
Normative state x input -> outcome table for time-field.vue
(@vizejs/ui/time-field). TimeField shares the DateField segment engine, so
DateField rows F2–F8, F10–F12, and F15 (native required validation) apply to hour, minute, second, and day-period
segments. Every row below is proven by the named test.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| T1 | locale clock | render | segment order, hour padding, and day-period labels follow Intl; hourCycle forces 12 or 24; granularity adds or removes minute/second segments |
renders locale clocks: 12-hour with a day period or 24-hour without |
| T2 | 12-hour, empty | digits and a/p |
hours, minutes, and the day period fill in order and commit a 24-hour PlainTime; letters and arrows toggle AM/PM |
typing hours, minutes, and a day period commits a 24-hour value |
| T3 | 12-hour | step | 12 AM is midnight and 12 PM is noon; hours wrap 12→1 without flipping the period; minutes wrap and Page steps by 15 | 12 AM and 12 PM map to midnight and noon; hours wrap within the clock |
| T4 | 24-hour, seconds | digits | two-digit hours wait for the second digit when it can fit; second granularity commits only after seconds; no hidden input without name |
24-hour typing accepts two-digit hours and seconds granularity requires seconds |
| T5 | min / max | commit / empty step | out-of-window times are invalid; placeholderValue seeds stepping from empty |
min and max mark times invalid; placeholderValue seeds empty stepping |
| T6 | disabled/read-only | keys | no edits are committed | disabled and read-only time fields block edits |
| T7 | imperative | expose / clock change | hourCycle, granularity, focus, setValue, clear; switching the clock re-renders the same time |
exposes hour cycle, granularity, and imperative editing |
| T8 | time model | helpers | PlainTime validation, comparison, truncation, and HH:MM[:SS] round-trips |
plain time helpers validate, compare, truncate, and round-trip ISO text |
| T9 | SSR/hydration | isolated requests | byte-identical segments and deterministic ids hydrate silently | renders byte-identical locale-ordered time segments across SSR requests, hydrates time segments with generated ids and no mismatches |
Root data adds data-hour-cycle (12/24) and data-granularity to the
DateField hooks, with data-vize-ui="time-field", time-field-segment, and
time-field-literal. The hidden input submits HH:MM, or HH:MM:SS for
second granularity.