TimeField
Time input field with labels, descriptions, and validation
Usage
<script setup lang="ts">
import { TimeField } from '@hareui/vue'
</script><script setup lang="ts">import { TimeField } from '@hareui/vue'</script><template> <TimeField class="w-[256px]" name="time" label="Time" /></template>Anatomy
TimeField renders a Label, a group holding an optional prefix, the segmented time input and an optional suffix, then a Description and a FieldError. The text parts come from props; each can be replaced with a slot.
<template>
<TimeField label="…" description="…" error-message="…" :invalid="…">
<template #label /> <!-- Label content -->
<template #prefix /> <!-- content rendered before the segments, e.g. an icon -->
<template #suffix /> <!-- content rendered after the segments -->
<template #description /> <!-- Description content -->
<template #error /> <!-- FieldError content, rendered when invalid -->
</TimeField>
</template>ui key | data-slot | Element |
|---|---|---|
base | time-field | <div> root |
| — | label | Label (iff label / #label) |
group | date-input-group | <div role="group"> holding the prefix, segments and suffix |
prefix | date-input-group-prefix | <span> (iff #prefix) |
input | date-input-group-input | <div> wrapping the segments |
segment | date-input-group-segment | one per time part (hour, minute, second, day period) |
suffix | date-input-group-suffix | <span> (iff #suffix) |
| — | description | Description (iff description / #description) |
| — | field-error | FieldError (while invalid, iff error-message / #error / validation errors) |
TimeField combines a label, segmented time input, description, and error into a single accessible component. Values are
@internationalized/dateTimeobjects — the same type HeroUI uses. It shares its input group with DateField.
Examples
With Icons
<script setup lang="ts">import { TimeField } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <TimeField class="w-[256px]" name="time" label="Time"> <template #prefix> <Icon class="size-4 text-muted" icon="gravity-ui:clock" /> </template> </TimeField></template><script setup lang="ts">import { TimeField } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <TimeField class="w-[256px]" name="time" label="Time"> <template #suffix> <Icon class="size-4 text-muted" icon="gravity-ui:clock" /> </template> </TimeField></template><script setup lang="ts">import { TimeField } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <TimeField class="w-[256px]" name="time" label="Time" description="Enter a time"> <template #prefix> <Icon class="size-4 text-muted" icon="gravity-ui:clock" /> </template> <template #suffix> <Icon class="size-4 text-muted" icon="gravity-ui:chevron-down" /> </template> </TimeField></template>On Surface
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
<script setup lang="ts">import { Surface, TimeField } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <Surface class="flex w-full max-w-sm flex-col gap-4 rounded-3xl p-6"> <TimeField class="w-full" variant="secondary" name="time" label="Time" description="Enter a time" /> <TimeField class="w-full" variant="secondary" name="time-2" label="Appointment time" description="Enter a time for your appointment"> <template #prefix> <Icon class="size-4 text-muted" icon="gravity-ui:clock" /> </template> </TimeField> </Surface></template>With Description
<script setup lang="ts">import { TimeField } from '@hareui/vue'</script><template> <div class="flex flex-col gap-4"> <TimeField class="w-[256px]" name="time" label="Start time" description="Enter the start time" /> <TimeField class="w-[256px]" name="end-time" label="End time" description="Enter the end time" /> </div></template>Required Field
<script setup lang="ts">import { TimeField } from '@hareui/vue'</script><template> <div class="flex flex-col gap-4"> <TimeField required class="w-[256px]" name="time" label="Time" /> <TimeField required class="w-[256px]" name="appointment-time" label="Appointment time" description="Required field" /> </div></template>Disabled State
<script setup lang="ts">import { getLocalTimeZone, now, Time } from '@internationalized/date'import { TimeField } from '@hareui/vue'const currentTime = now(getLocalTimeZone())const timeValue = new Time(currentTime.hour, currentTime.minute, currentTime.second)</script><template> <div class="flex flex-col gap-4"> <TimeField disabled class="w-[256px]" name="time" label="Time" :default-value="timeValue" description="This time field is disabled" /> <TimeField disabled class="w-[256px]" name="time-empty" label="Time" description="This time field is disabled" /> </div></template>Full Width
<script setup lang="ts">import { TimeField } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <div class="w-[400px] space-y-4"> <TimeField full-width name="time" label="Time" /> <TimeField full-width name="time-icons" label="Time"> <template #prefix> <Icon class="size-4 text-muted" icon="gravity-ui:clock" /> </template> <template #suffix> <Icon class="size-4 text-muted" icon="gravity-ui:chevron-down" /> </template> </TimeField> </div></template>Validation
Set invalid with error-message to control the state yourself, or validate with required, a validate function, or a surrounding Form.
<script setup lang="ts">import { TimeField } from '@hareui/vue'</script><template> <div class="flex flex-col gap-4"> <TimeField invalid required class="w-[256px]" name="time" label="Time" error-message="Please enter a valid time" /> <TimeField invalid class="w-[256px]" name="invalid-time" label="Time" error-message="Time must be within business hours" /> </div></template>Controlled
Bind the value with v-model to synchronize with other components or state management.
<script setup lang="ts">import type { TimeValue } from 'reka-ui'import { getLocalTimeZone, now, Time } from '@internationalized/date'import { Button, TimeField } from '@hareui/vue'import { shallowRef } from 'vue'const value = shallowRef<TimeValue | null>(null)function setNow() { const currentTime = now(getLocalTimeZone()) value.value = new Time(currentTime.hour, currentTime.minute, currentTime.second)}</script><template> <div class="flex flex-col gap-4"> <TimeField v-model="value" class="w-[256px]" name="time" label="Time" :description="`Current value: ${value ? value.toString() : '(empty)'}`" /> <div class="flex gap-2"> <Button variant="tertiary" @click="setNow"> Set now </Button> <Button variant="tertiary" @click="value = null"> Clear </Button> </div> </div></template>Form Example
Complete form example with validation and submission handling.
<script setup lang="ts">import type { Time } from '@internationalized/date'import { parseTime } from '@internationalized/date'import { Button, Form, TimeField } from '@hareui/vue'import { Icon } from '@iconify/vue'import { computed, ref, shallowRef } from 'vue'const value = shallowRef<Time | null>(null)const isSubmitting = ref(false)const minTime = parseTime('09:00')const maxTime = parseTime('17:00')const isInvalid = computed(() => value.value !== null && (value.value.compare(minTime) < 0 || value.value.compare(maxTime) > 0))function handleSubmit(e: Event) { e.preventDefault() if (!value.value || isInvalid.value) return isSubmitting.value = true // Simulate API call setTimeout(() => { console.log('Time submitted:', { time: value.value }) value.value = null isSubmitting.value = false }, 1500)}</script><template> <Form class="flex w-[280px] flex-col gap-4" @submit="handleSubmit"> <TimeField v-model="value" required class="w-full" :invalid="isInvalid" :min-value="minTime" :max-value="maxTime" name="time" label="Appointment time" error-message="Time must be between 9:00 AM and 5:00 PM" description="Enter a time between 9:00 AM and 5:00 PM" > <template #prefix> <Icon class="size-4 text-muted" icon="gravity-ui:clock" /> </template> </TimeField> <Button class="w-full" :disabled="!value || isInvalid" :pending="isSubmitting" type="submit" variant="primary"> {{ isSubmitting ? 'Submitting...' : 'Submit' }} </Button> </Form></template>With Validation
TimeField supports validation with min-value, max-value, and custom validation logic.
<script setup lang="ts">import type { Time } from '@internationalized/date'import { parseTime } from '@internationalized/date'import { TimeField } from '@hareui/vue'import { computed, shallowRef } from 'vue'const value = shallowRef<Time | null>(null)const minTime = parseTime('09:00')const maxTime = parseTime('17:00')const isInvalid = computed(() => value.value !== null && (value.value.compare(minTime) < 0 || value.value.compare(maxTime) > 0))</script><template> <div class="flex flex-col gap-4"> <TimeField v-model="value" required class="w-[256px]" :invalid="isInvalid" :min-value="minTime" :max-value="maxTime" name="time" label="Time" error-message="Time must be between 9:00 AM and 5:00 PM" description="Enter a time between 9:00 AM and 5:00 PM" /> </div></template>Render Function
HeroUI's render prop replaces the root element and lets sub-parts render-prop their own markup. Option A has no compound parts to render-prop into; attributes you pass to TimeField fall through to the root element instead.
<script setup lang="ts">import { TimeField } from '@hareui/vue'</script><template> <!-- HeroUI's render-function demo shows a render prop customizing each sub-part; Option A has no compound parts to render-prop into. Vue's natural attrs fallthrough reaches the same root element instead, so this demo shows an arbitrary attribute landing on the field. --> <TimeField data-custom="foo" class="w-[256px]" name="time" label="Time" /></template>Customization
Tailwind CSS
<script setup lang="ts">import { TimeField } from '@hareui/vue'</script><template> <!-- HeroUI's custom-styles demo places `<Description>` above `<TimeField.Group>` via its compound children order; Option A's `description` prop always renders after the field's anatomy (same position as every other description demo), so the description sits below the group here. --> <TimeField class="w-full max-w-48 gap-1.5" name="reminder" description="Daily check-in notification." :ui="{ group: 'rounded-xl border border-border/80 bg-surface px-2 py-1 shadow-sm ring-1 ring-accent/5 focus-within:border-accent/25 focus-within:ring-2 focus-within:ring-accent/15', segment: 'text-foreground', }" > <template #label> <span class="font-medium text-foreground">Reminder time</span> </template> </TimeField></template>Global Configuration
app.use(createHareUI({
ui: { timeField: { slots: { group: 'rounded-xl', segment: 'font-mono' } } },
}))Styling Reference
Slots
base→[data-slot="time-field"]– root container (flex flex-col gap-1)group→[data-slot="date-input-group"]– container for the prefix, segments and suffix, with border and background stylingprefix→[data-slot="date-input-group-prefix"]– content before the segments, e.g. an iconinput→[data-slot="date-input-group-input"]– wrapper around the segmentssegment→[data-slot="date-input-group-segment"]– each individual time partsuffix→[data-slot="date-input-group-suffix"]– content after the segments
Note: The child components (Label, Description, FieldError) have their own themes. See their respective pages for customization options.
group,input,segment,prefixandsuffixall belong to the internaldate-input-grouppart shared with DateField.
Interactive States
TimeField sets these data attributes:
- Invalid:
[data-invalid="true"]on the root and the group - Automatically hides the description when invalid - Required:
[data-required="true"]on the root - Shows the label's required asterisk - Disabled:
[data-disabled="true"]on the root,[data-disabled]on the group and each segment - Read Only:
[data-readonly="true"]on the root - Focus Within:
[data-focus-within="true"]on the group - Applied when a segment is focused - Segment Focus:
:focusor[data-focused="true"]on a segment - Segment Placeholder:
[data-placeholder]on a segment showing its placeholder text
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Label text rendered above the group. |
description | string | - | Helper text rendered below the group. Hidden while the field is invalid. |
errorMessage | string | - | Error message rendered below the group while invalid. Defaults to the validation errors. |
id | string | useId() | The id of the field; label, description and error ids derive from it. |
name | string | - | The name of the value, used when submitting a form. |
defaultValue | TimeValue | - | The initial value when uncontrolled (no v-model). |
placeholder | TimeValue | - | The placeholder time, used to determine which segments to show when no time is selected. Use v-model:placeholder to control it. |
defaultPlaceholder | TimeValue | - | The placeholder time when uncontrolled. |
granularity | "hour" | "minute" | "second" | 'minute' | The granularity of the field: which segments are rendered, up to and including this unit. |
hourCycle | HourCycle | - | The hour cycle used to format the time. Defaults to the locale's preference. |
step | DateStep | 1 | The stepping interval for the time segments. |
stepSnapping | boolean | false | Whether to snap the value to the nearest step increment after input. |
hideTimeZone | boolean | - | Whether to hide the time zone segment. |
minValue | TimeValue | - | The minimum selectable time. |
maxValue | TimeValue | - | The maximum selectable time. |
locale | string | - | The locale used to format the field. Defaults to the ConfigProvider locale. |
variant | "primary" | "secondary" | 'primary' | Visual variant of the group. |
fullWidth | boolean | false | Whether the field takes the full width of its container. |
disabled | boolean | false | Whether the field is disabled. |
invalid | boolean | undefined | Whether the field is invalid. Overrides validation when set. |
required | boolean | false | Whether the field is required. |
readonly | boolean | false | Whether the value can be selected but not changed. |
validate | ValidateFn<TimeValue> | - | Validates the committed value. Return an error message (or several) when invalid. |
validationBehavior | ValidationBehavior | 'native' | native blocks form submission and shows errors on commit or submit; aria shows errors in realtime.
Defaults to the surrounding Form. |
ui | (ComponentSlots<{ slots: { base: string[]; }; variants: { fullWidth: { false: { base: string; }; true: { base: string; }; }; }; defaultVariants: { fullWidth: boolean; }; }> & { group?: C; } & Pick<ComponentSlots<{ slots: { base: string[]; inputContainer: string; input: string[]; segment: string[]; prefix: string; suffix: string; }; variants: { variant: { primary: { base: string; }; secondary: { base: string[]; }; }; fullWidth: { false: { base: string; }; true: { base: string; }; }; }; defaultVariants: { fullWidth: boolean; variant: string; }; }>, "input" | "prefix" | "suffix" | "segment">) | - | Per-slot class overrides. group, input, segment, prefix and suffix are the internal date-input-group's. |
modelValue | TimeValue | null | - | The selected time. Bind with v-model. |
Slots
| Slot | Props | Description |
|---|---|---|
label | any | Label content. Replaces the label prop. |
prefix | any | Prefix content, rendered before the segments (e.g. an icon). |
suffix | any | Suffix content, rendered after the segments (e.g. an icon). |
description | any | Description content. Replaces the description prop. |
error | ValidationResult | Error content, rendered while invalid. Replaces the errorMessage prop. |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | [value: TimeValue | null | undefined] | - |
update:placeholder | [value: TimeValue] | - |
Accessibility
- The label's
forpoints to the field's id (auto-generated withuseId()unlessidis set) - The field's
aria-describedbylists the ids of the rendered description (<id>-description) and error (<id>-error) invalidsetsaria-invalid="true"on the field; the error is announced viarole="alert"- Keyboard: ←/→ move between segments, ↑/↓ increment/decrement the focused segment, digits type directly into the focused segment and auto-advance
min-valueandmax-valuemark the field invalid on their own, matching React Aria'suseTimeFieldState— no need to computeinvalidyourself just for native-range feedback







