TimeField

Time input field with labels, descriptions, and validation

Usage

<script setup lang="ts">
import { TimeField } from '@hareui/vue'
</script>
Time
––
––
AM
<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 keydata-slotElement
basetime-field<div> root
—labelLabel (iff label / #label)
groupdate-input-group<div role="group"> holding the prefix, segments and suffix
prefixdate-input-group-prefix<span> (iff #prefix)
inputdate-input-group-input<div> wrapping the segments
segmentdate-input-group-segmentone per time part (hour, minute, second, day period)
suffixdate-input-group-suffix<span> (iff #suffix)
—descriptionDescription (iff description / #description)
—field-errorFieldError (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/date Time objects — the same type HeroUI uses. It shares its input group with DateField.

Examples

With Icons

Time
––
––
AM
<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>
Time
––
––
AM
<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>
Time
––
––
AM
Enter a time
<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.

Time
––
––
AM
Enter a time
Appointment time
––
––
AM
Enter a time for your appointment
<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

Start time
––
––
AM
Enter the start time
End time
––
––
AM
Enter the end time
<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

Time
––
––
AM
Appointment time
––
––
AM
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

Time
9
35
AM
This time field is disabled
Time
––
––
AM
This time field is disabled
<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

Time
––
––
AM
Time
––
––
AM
<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.

Time
––
––
AM
Time
––
––
AM
<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.

Time
––
––
AM
Current value: (empty)
<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.

Appointment time
––
––
AM
Enter a time between 9:00 AM and 5:00 PM
<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.

Time
––
––
AM
Enter a time between 9:00 AM and 5:00 PM
<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.

Time
––
––
AM
<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

Reminder time
––
––
AM
Daily check-in notification.
<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 styling
  • prefix → [data-slot="date-input-group-prefix"] – content before the segments, e.g. an icon
  • input → [data-slot="date-input-group-input"] – wrapper around the segments
  • segment → [data-slot="date-input-group-segment"] – each individual time part
  • suffix → [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, prefix and suffix all belong to the internal date-input-group part 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: :focus or [data-focused="true"] on a segment
  • Segment Placeholder: [data-placeholder] on a segment showing its placeholder text

API Reference

Props

PropTypeDefaultDescription
labelstring-Label text rendered above the group.
descriptionstring-Helper text rendered below the group. Hidden while the field is invalid.
errorMessagestring-Error message rendered below the group while invalid. Defaults to the validation errors.
idstringuseId()The id of the field; label, description and error ids derive from it.
namestring-The name of the value, used when submitting a form.
defaultValueTimeValue-The initial value when uncontrolled (no v-model).
placeholderTimeValue-The placeholder time, used to determine which segments to show when no time is selected. Use v-model:placeholder to control it.
defaultPlaceholderTimeValue-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.
hourCycleHourCycle-The hour cycle used to format the time. Defaults to the locale's preference.
stepDateStep1The stepping interval for the time segments.
stepSnappingbooleanfalseWhether to snap the value to the nearest step increment after input.
hideTimeZoneboolean-Whether to hide the time zone segment.
minValueTimeValue-The minimum selectable time.
maxValueTimeValue-The maximum selectable time.
localestring-The locale used to format the field. Defaults to the ConfigProvider locale.
variant"primary" | "secondary"'primary'Visual variant of the group.
fullWidthbooleanfalseWhether the field takes the full width of its container.
disabledbooleanfalseWhether the field is disabled.
invalidbooleanundefinedWhether the field is invalid. Overrides validation when set.
requiredbooleanfalseWhether the field is required.
readonlybooleanfalseWhether the value can be selected but not changed.
validateValidateFn<TimeValue>-Validates the committed value. Return an error message (or several) when invalid.
validationBehaviorValidationBehavior'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.
modelValueTimeValue | null-The selected time. Bind with v-model.

Slots

SlotPropsDescription
labelanyLabel content. Replaces the label prop.
prefixanyPrefix content, rendered before the segments (e.g. an icon).
suffixanySuffix content, rendered after the segments (e.g. an icon).
descriptionanyDescription content. Replaces the description prop.
errorValidationResultError content, rendered while invalid. Replaces the errorMessage prop.

Emits

EventPayloadDescription
update:modelValue[value: TimeValue | null | undefined]-
update:placeholder[value: TimeValue]-

Accessibility

  • The label's for points to the field's id (auto-generated with useId() unless id is set)
  • The field's aria-describedby lists the ids of the rendered description (<id>-description) and error (<id>-error)
  • invalid sets aria-invalid="true" on the field; the error is announced via role="alert"
  • Keyboard: ←/→ move between segments, ↑/↓ increment/decrement the focused segment, digits type directly into the focused segment and auto-advance
  • min-value and max-value mark the field invalid on their own, matching React Aria's useTimeFieldState — no need to compute invalid yourself just for native-range feedback

On this page

No Headings