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>
Usage
import { TimeField } from '@hina-ui/vue'
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.
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>
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.
<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>
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.
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>
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.
<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>
Clearable
clearable adds a button at the end that empties the value to null and returns focus to the first segment.
<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>
Sizes
size has three steps, sm, md and lg, matching the input of the same step in height.
<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>
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.
<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>
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.
<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>
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
minormaxare 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-labeloraria-labelledby. invalidand out-of-range values both setaria-invalidon 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 |