TimeRangeField
Overview
A segmented time range input with independently editable start and end time segments (hour, minute, and optional second and day-period) while still submitting native form values for both sides. Use it whenever a user must enter a start and end time quickly and precisely — scheduling, booking, shift or time-window filtering. For a single time use STimeField; for a date and time range use SDateRangeField with a time granularity or SDateRangePicker.
Usage
Features
- ⏱ Two segmented groups — independent start/end hour, minute, and optional second and day-period segments, each an editable
role="spinbutton" - 🎚 Controlled / uncontrolled —
v-model/defaultValuefor the whole{ start, end }range, backed byuseControllableState - 🕛 12/24-hour cycle —
hourCycleswitches betweenAM/PMand 24-hour display per locale, with day-period segment editing on both sides - ⌨️ Full keyboard support — digit typing with auto-advance,
ArrowUp/ArrowDownincrement/decrement,ArrowLeft/ArrowRightnavigation that crosses group boundaries and reverses in RTL - ⛓ Range validation —
start ≤ endis enforced, plusminValue/maxValue/isTimeUnavailablemark the rootdata-invalid - 📝 Two native form values — visually hidden
input[type="time"]elements carry both sides understartName/endName - ⚙️ Granularity & step —
granularity(hour/minute/second) andstepcontrol which segments render and the increment per arrow - 🧩 Composable slots —
leading/trailingplace content around the groups andseparatorcustomizes the divider between them - 🚫 Disabled / readonly — both states block editing on every segment and render proper
data-*/aria-*attributes
Component family
STimeRangeField— the styled wrapper that forwards props to the Aria compact and injectsdateRangeFieldVariants(shared withSDateRangeField) classesTimeRangeFieldCompact(Aria) — data-driven composition ofTimeRangeFieldRoot+ oneTimeRangeFieldInputper segment in two groups, plusleading/separator/trailingslots; import from@vean/aria/time-range-fieldfor unstyled usageTimeRangeFieldRoot/TimeRangeFieldInput(Aria) — the state owner (segment values, range validation, two hidden inputs, cross-group focus management) and a single editable segment bound to the shareduseDateFieldcomposable
Demos
Basic
Custom Styling
API
Aria Layer
4TimeRangeField
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<TimeRangeFieldUi> | — | Per-slot class overrides for the component. |
inputProps | TimeRangeFieldInputProps | — | Properties forwarded to the input element. |
separator | string | — | Separator. |
defaultValue | TimeRange | — | Default value. |
modelValue | TimeRange | — | Current model value. |
startName | string | — | Start name. |
endName | string | — | End name. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
dir | Direction | — | Reading direction of the component. |
id | string | — | Id. |
placeholder | Date | — | Placeholder. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
readonly | boolean | — | Whether the component is readonly. |
step | DateStep | — | Step. |
locale | string | — | Locale. |
defaultPlaceholder | Date | — | Default placeholder. |
maxValue | Date | — | Max value. |
minValue | Date | — | Min value. |
granularity | TimeGranularity | — | Granularity. |
hourCycle | HourCycle | — | Hour cycle. |
hideTimeZone | boolean | — | Whether hide time zone. |
isTimeUnavailable | TimeMatcher | — | Whether the time is unavailable. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | TimeRange | Emitted when the model value changes. |
update:placeholder | Date | Emitted when the placeholder value changes. |
update:startValue | Date | null | Emitted when the start value changes. |
update:endValue | Date | null | Emitted when the end value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
leading | — | Custom content for the leading slot. |
trailing | — | Custom content for the trailing slot. |
separator | — | Custom content for the separator slot. |
TimeRangeFieldCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
inputProps | TimeRangeFieldInputProps | — | Properties forwarded to the input element. |
separator | string | — | Separator. |
defaultValue | TimeRange | — | Default value. |
modelValue | TimeRange | — | Current model value. |
startName | string | — | Start name. |
endName | string | — | End name. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
dir | Direction | — | Reading direction of the component. |
id | string | — | Id. |
placeholder | Date | — | Placeholder. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
readonly | boolean | — | Whether the component is readonly. |
step | DateStep | — | Step. |
locale | string | — | Locale. |
defaultPlaceholder | Date | — | Default placeholder. |
maxValue | Date | — | Max value. |
minValue | Date | — | Min value. |
granularity | TimeGranularity | — | Granularity. |
hourCycle | HourCycle | — | Hour cycle. |
hideTimeZone | boolean | — | Whether hide time zone. |
isTimeUnavailable | TimeMatcher | — | Whether the time is unavailable. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | TimeRange | Emitted when the model value changes. |
update:placeholder | Date | Emitted when the placeholder value changes. |
update:startValue | Date | null | Emitted when the start value changes. |
update:endValue | Date | null | Emitted when the end value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
leading | — | Custom content for the leading slot. |
trailing | — | Custom content for the trailing slot. |
separator | — | Custom content for the separator slot. |
TimeRangeFieldInput
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'start' | 'end' | — | Type. |
part | SegmentPart | — | Part. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
TimeRangeFieldRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | TimeRange | — | Default value. |
modelValue | TimeRange | — | Current model value. |
startName | string | — | Start name. |
endName | string | — | End name. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
dir | Direction | — | Reading direction of the component. |
id | string | — | Id. |
placeholder | Date | — | Placeholder. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
readonly | boolean | — | Whether the component is readonly. |
step | DateStep | — | Step. |
locale | string | — | Locale. |
defaultPlaceholder | Date | — | Default placeholder. |
maxValue | Date | — | Max value. |
minValue | Date | — | Min value. |
granularity | TimeGranularity | — | Granularity. |
hourCycle | HourCycle | — | Hour cycle. |
hideTimeZone | boolean | — | Whether hide time zone. |
isTimeUnavailable | TimeMatcher | — | Whether the time is unavailable. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | TimeRange | Emitted when the model value changes. |
update:placeholder | Date | Emitted when the placeholder value changes. |
update:startValue | Date | null | Emitted when the start value changes. |
update:endValue | Date | null | Emitted when the end value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | TimeRangeFieldSlotProps | Custom content for the default slot. |
Notes
Architecture and benchmark differences
TimeRangeFieldRoot owns the TimeRange via useControllableState, keeps startSegmentValues/endSegmentValues as two shallowRefs, and runs range validation through isInvalid (start after end, minValue/maxValue, isTimeUnavailable). Each TimeRangeFieldInput binds the same useDateField composable used by the date-field family; TimeRangeFieldCompact iterates the segments produced by createContent and renders one input per segment inside two data-time-range-field-part groups separated by the separator slot. Cross-group focus movement is handled by the root’s moveFocus, mapping the physical arrow key from dir so RTL swaps ArrowLeft/ArrowRight — the group boundary is crossed on the physical direction (delta), not the semantic key. Most benchmark libraries ship two independent text inputs (or a plain picker); the dual segmented spinbutton groups with cross-boundary keyboard navigation and the Aria/styled split are the differentiators.
| Capability | VeanUI | Ant Design | Element Plus | Mantine | Naive UI | shadcn |
|---|---|---|---|---|---|---|
| Aria/styled split | ✅ | — | — | — | — | — |
| Dual segmented editable groups | ✅ | — | — | — | — | — |
| Cross-group focus movement | ✅ | — | — | — | — | — |
| RTL direction reversal | ✅ | — | — | — | — | — |
| Controlled / uncontrolled | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Keyboard increment/typing | ✅ | — | — | — | — | — |
| Range validation (start ≤ end) | ✅ | ✅ | ✅ | ✅ | ✅ | — |
isTimeUnavailable | ✅ | — | — | — | — | — |
| Granularity (second) | ✅ | — | — | — | — | — |
| Two native form values | ✅ | ✅ | ✅ | ✅ | — | — |
| Separator prop / slot | ✅ | — | — | — | — | — |
| Disabled / readonly | ✅ | ✅ | ✅ | ✅ | ✅ | — |
Cautions
- The value is a
TimeRange—{ start, end }ofTimeValues (nativeDates whose time-of-day parts are meaningful), notstrings. Use it with the other date-family components. defaultValueis only read on mount — usev-modelfor external control.- When a side is empty its segments display the placeholder time (derived from
defaultValue/defaultPlaceholderor the current time) — the submitted form value is still empty until the user types. granularitydefaults to'minute'; pass'second'for seconds segments on both sides.hourCycle(12/24) controls the day-period segments and the accessible hour range; leave it unset to follow the locale.- Range validation marks the root
data-invalidbut never clamps the value — clear or correct it in the parent. - The two hidden inputs share
required; if you need per-side submit names passstartName/endName(falling back tonamefor the start side).
FAQ
How do I edit the range with the keyboard?
Tab into either group and type digits — after a segment fills it advances automatically. ArrowUp/ArrowDown increment or decrement the focused segment, and ArrowLeft/ArrowRight move between segments — including across the start/end boundary — reversed in RTL. On a 12-hour cycle a/p toggles AM/PM.
How do I show seconds on both sides?
Pass granularity: 'second' to render hour, minute, and second segments in the start and end groups.
How do I customize the separator between start and end?
Pass separator: '→' as a prop for a simple text divider, or use the separator slot to render any content (an icon, for example).
How do I enforce that end is after start?
The component marks the root data-invalid automatically when end is before start. Combine with minValue/maxValue/isTimeUnavailable for arbitrary constraints — validation never clamps the value, so correct it in the parent.
How is STimeRangeField different from SDateRangeField?
STimeRangeField is a pure time range input — it never renders date segments. SDateRangeField renders full dates (with optional time via granularity) and accepts DateValue pairs.