TimeField

Types a time segment by segment.

8
00
PM

Value: 20:00

<script setup lang="ts">
  import { ref } from 'vue'
  import { Clock } from '@lucide/vue'
  import { Stack, Text, TimeField } from '@hina-ui/vue'

  const time = ref<string | null>('20:00')
</script>

<template>
  <Stack gap="sm" align="start">
    <TimeField v-model="time" clearable aria-label="Stream start" class="w-44">
      <template #leading>
        <Clock />
      </template>
    </TimeField>
    <Text tone="muted" size="sm">Value: {{ time ?? 'empty' }}</Text>
  </Stack>
</template>
vue

Usage

import { TimeField } from '@hina-ui/vue'
ts

The time field splits a time into hour and minute segments. Each segment accepts digits only, moves on to the next once it is complete, and the up and down arrow keys step it. v-model binds an HH:mm string, or HH:mm:ss at second precision, the same shape as the browser’s native time input. Segment order, separators and placeholders follow the current locale, and 12-hour locales add an AM/PM segment. Attributes it does not declare land on the group element that wraps the segments, so name it with aria-label or aria-labelledby.

––
––
AM

Value: empty

<script setup lang="ts">
  import { ref } from 'vue'
  import { Stack, Text, TimeField } from '@hina-ui/vue'

  const time = ref<string | null>(null)
</script>

<template>
  <Stack gap="sm" align="start">
    <TimeField v-model="time" aria-label="Stream start" class="w-40" />
    <Text tone="muted" size="sm">Value: {{ time ?? 'empty' }}</Text>
  </Stack>
</template>
vue

Examples

Granularity

granularity sets the precision: the default minute has hour and minute, second adds seconds and hour has the hour only. hourCycle picks the 12 or 24 hour clock, defaulting to the locale.

8
PM
8
00
30
PM
8
00
PM
<script setup lang="ts">
  import { ref } from 'vue'
  import { Stack, TimeField } from '@hina-ui/vue'

  const hour = ref<string | null>('20:00')
  const second = ref<string | null>('20:00:30')
  const twelve = ref<string | null>('20:00')
</script>

<template>
  <Stack class="w-56">
    <TimeField v-model="hour" granularity="hour" aria-label="Whole hour" />
    <TimeField v-model="second" granularity="second" aria-label="Seconds" />
    <TimeField v-model="twelve" :hour-cycle="12" aria-label="12-hour clock" />
  </Stack>
</template>
vue

Minute step

minuteStep steps the minute segment by a fixed amount and snaps typed minutes to the nearest step, for slots planned by the quarter or half hour.

9
00
AM

Value: 09:00

<script setup lang="ts">
  import { ref } from 'vue'
  import { Stack, Text, TimeField } from '@hina-ui/vue'

  const time = ref<string | null>('09:00')
</script>

<template>
  <Stack gap="sm" align="start">
    <TimeField v-model="time" :minute-step="15" aria-label="Booking slot" class="w-40" />
    <Text tone="muted" size="sm">Value: {{ time ?? 'empty' }}</Text>
  </Stack>
</template>
vue

Range

min and max bound the accepted range; a value outside it shows as invalid but is still emitted. placeholder sets the time the segments start from while the field is empty, the current time by default.

9
30
PM
<script setup lang="ts">
  import { ref } from 'vue'
  import { TimeField } from '@hina-ui/vue'

  const time = ref<string | null>('21:30')
</script>

<template>
  <TimeField
    v-model="time"
    min="09:00"
    max="18:00"
    placeholder="09:00"
    aria-label="Opening hours"
    class="w-40"
  />
</template>
vue

Clearable

clearable adds a button at the end that empties the value to null and returns focus to the first segment.

8
00
PM
<script setup lang="ts">
  import { ref } from 'vue'
  import { TimeField } from '@hina-ui/vue'

  const time = ref<string | null>('20:00')
</script>

<template>
  <TimeField v-model="time" clearable aria-label="Stream start" class="w-40" />
</template>
vue

Sizes

size has three steps, sm, md and lg, matching the input of the same step in height.

8
00
PM
8
00
PM
8
00
PM
<script setup lang="ts">
  import { Stack, TimeField } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-40">
    <TimeField size="sm" model-value="20:00" aria-label="Small" />
    <TimeField size="md" model-value="20:00" aria-label="Medium" />
    <TimeField size="lg" model-value="20:00" aria-label="Large" />
  </Stack>
</template>
vue

States

invalid gives the field a warning color, disabled disables the group and readonly keeps it focusable but not editable. variant="secondary" is the flat form used inside a surface.

8
00
PM
8
00
PM
8
00
PM
8
00
PM
<script setup lang="ts">
  import { Stack, TimeField } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-40">
    <TimeField model-value="20:00" invalid aria-label="Invalid" />
    <TimeField model-value="20:00" disabled aria-label="Disabled" />
    <TimeField model-value="20:00" readonly aria-label="Read only" />
    <TimeField model-value="20:00" variant="secondary" aria-label="Secondary" />
  </Stack>
</template>
vue

In a form

Inside a FormField the label is linked to the whole time input through aria-labelledby, and the error message is rendered by the field; validation rules and submission belong to the Form. The value is an HH:mm string, so it can be compared as a string.

––
––
AM
<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Form, FormField, Text, TimeField } from '@hina-ui/vue'

  const schema = v.object({
    opensAt: v.pipe(
      v.string('Enter the opening time'),
      v.check(value => value >= '06:00', 'Opening is no earlier than 06:00'),
    ),
  })

  const values = reactive({ opensAt: null as string | null })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = JSON.stringify(data)
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="opensAt" label="Opening time" required>
      <TimeField v-model="values.opensAt" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">Save</Button>
    <Text v-if="saved" tone="muted" size="sm">Saved: {{ saved }}</Text>
  </Form>
</template>
vue

Behavior

  • Clicking a segment starts typing; a click on the blank area focuses the first empty segment.
  • Each segment accepts digits only and moves on once complete; the left and right arrow keys move between segments, up and down step the value and Backspace clears the current segment.
  • A value is emitted only when every segment is filled; clearing any segment makes the value null.
  • Values outside min or max are still emitted and marked invalid.

Accessibility

  • Segments are role="spinbutton" with localized names and current values; separators are hidden from assistive technology.
  • Name the group with aria-label or aria-labelledby.
  • invalid and out-of-range values both set aria-invalid on the group element.

API

Props

Prop
Type
Default
Description
modelValue
string | null
null
An HH:mm string, HH:mm:ss at second precision
placeholder
string
Current time
Where the segments start from while empty, HH:mm
min
string
—
Lower bound of the accepted range
max
string
—
Upper bound of the accepted range
granularity
'hour' | 'minute' | 'second'
'minute'
Precision
hourCycle
12 | 24
Locale
Hour cycle
minuteStep
number
—
Step of the minute segment
clearable
boolean
false
Whether the clear button is shown
readonly
boolean
false
Whether the field is read only
name
string
—
Form field name
variant
'primary' | 'secondary'
'primary'
Variant
size
'sm' | 'md' | 'lg'
'md'
Size
disabled
boolean
false
Whether the field is disabled
invalid
boolean
false
Whether validation failed
class
string
—
Classes appended to the root element

Slots

Slot
Description
leading
Content of the leading square
trailing
Content of the trailing square

Events

Event
Payload
Description
update:modelValue
value: string | null
The value changed
clear
—
The clear was clicked