Value: 2026-09-04
<script setup lang="ts">
import { ref } from 'vue'
import { CalendarDays } from '@lucide/vue'
import { DateField, Stack, Text } from '@hina-ui/vue'
const date = ref<string | null>('2026-09-04')
</script>
<template>
<Stack gap="sm" align="start">
<DateField v-model="date" clearable aria-label="Publish date" class="w-72">
<template #leading>
<CalendarDays />
</template>
</DateField>
<Text tone="muted" size="sm">Value: {{ date ?? 'empty' }}</Text>
</Stack>
</template>
Usage
import { DateField } from '@hina-ui/vue'
The date field splits a date into year, month and day 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 ISO 8601 string: YYYY-MM-DD for dates and YYYY-MM-DDTHH:mm with time, the same shape as the browser’s native date inputs, ready to submit or store. Segment order, separators and placeholders follow the current locale. 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 { DateField, Stack, Text } from '@hina-ui/vue'
const date = ref<string | null>(null)
</script>
<template>
<Stack gap="sm" align="start">
<DateField v-model="date" aria-label="Release date" class="w-72" />
<Text tone="muted" size="sm">Value: {{ date ?? 'empty' }}</Text>
</Stack>
</template>
Examples
Date and time
granularity sets the precision: the default day is date only, minute adds hour and minute, second adds seconds and hour stops at the hour. hourCycle picks the 12 or 24 hour clock, defaulting to the locale.
Value: 2026-09-04T20:00
<script setup lang="ts">
import { ref } from 'vue'
import { DateField, Stack, Text } from '@hina-ui/vue'
const at = ref<string | null>('2026-09-04T20:00')
</script>
<template>
<Stack gap="sm" align="start">
<DateField v-model="at" granularity="minute" aria-label="Schedule" class="w-80" />
<Text tone="muted" size="sm">Value: {{ at ?? 'empty' }}</Text>
</Stack>
</template>
Range
min and max bound the accepted range; a value outside it puts the field into the invalid state but is still emitted. placeholder sets the date the segments start from while the field is empty, today by default.
<script setup lang="ts">
import { ref } from 'vue'
import { DateField } from '@hina-ui/vue'
const date = ref<string | null>('2026-10-15')
</script>
<template>
<DateField
v-model="date"
min="2026-09-01"
max="2026-09-30"
placeholder="2026-09-01"
aria-label="Event date"
class="w-72"
/>
</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 { DateField } from '@hina-ui/vue'
const date = ref<string | null>('2026-09-04')
</script>
<template>
<DateField v-model="date" clearable aria-label="Publish date" class="w-72" />
</template>
Sizes
size has three steps, sm, md and lg, matching the input of the same step in height.
<script setup lang="ts">
import { DateField, Stack } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-72">
<DateField size="sm" model-value="2026-09-04" aria-label="Small" />
<DateField size="md" model-value="2026-09-04" aria-label="Medium" />
<DateField size="lg" model-value="2026-09-04" 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 { DateField, Stack } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-72">
<DateField model-value="2026-09-04" invalid aria-label="Invalid" />
<DateField model-value="2026-09-04" disabled aria-label="Disabled" />
<DateField model-value="2026-09-04" readonly aria-label="Read only" />
<DateField model-value="2026-09-04" variant="secondary" aria-label="Secondary" />
</Stack>
</template>
In a form
Inside a FormField the label is linked to the whole date input through aria-labelledby, and the description and error message are rendered by the field; validation rules and submission belong to the Form. The value is an ISO 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, DateField, Form, FormField, Text } from '@hina-ui/vue'
const today = new Date().toISOString().slice(0, 10)
const schema = v.object({
birthday: v.pipe(
v.string('Enter your date of birth'),
v.check(value => value <= today, 'The date of birth cannot be in the future'),
),
})
const values = reactive({ birthday: 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="birthday" label="Date of birth" required>
<DateField v-model="values.birthday" />
</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 or the blank area of the field 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
- Each segment is
role="spinbutton"with a localized name and its current value; 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 | ISO 8601 string, precision per granularity |
placeholder | string | today | Starting point of the empty segments |
min | string | — | Lower bound of the accepted range |
max | string | — | Upper bound of the accepted range |
granularity | 'day' | 'hour' | 'minute' | 'second' | 'day' | Precision |
hourCycle | 12 | 24 | locale | Hour cycle |
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 group is disabled |
invalid | boolean | false | Whether the field failed validation |
class | string | — | Classes appended to the root element |
Slots
| Slot | Description |
|---|---|
leading | Content of the leading adornment |
trailing | Content of the trailing adornment |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: string | null | The value changed |
clear | — | The clear was used |