Value: 2026-09-04T20:00
<script setup lang="ts">
import { ref } from 'vue'
import { DateTimePicker, Stack, Text } from '@hina-ui/vue'
const at = ref<string | null>('2026-09-04T20:00')
</script>
<template>
<Stack gap="sm" align="start">
<DateTimePicker v-model="at" clearable aria-label="Publish time" class="w-80" />
<Text tone="muted" size="sm">Value: {{ at ?? 'empty' }}</Text>
</Stack>
</template>
Usage
import { DateTimePicker } from '@hina-ui/vue'
The date time picker combines a minute-precision DateField with a Calendar and a TimeField in a popover: type straight into the segments, or click the button at the end to pick the date in the calendar and adjust the time in the time field. v-model binds a YYYY-MM-DDTHH:mm ISO 8601 string, YYYY-MM-DDTHH:mm:ss at second precision, the same shape as the browser’s native date time input. 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 { DateTimePicker, Stack, Text } from '@hina-ui/vue'
const at = ref<string | null>(null)
</script>
<template>
<Stack gap="sm" align="start">
<DateTimePicker
v-model="at"
placeholder="2026-09-01T09:00"
aria-label="Schedule"
class="w-80"
/>
<Text tone="muted" size="sm">Value: {{ at ?? 'empty' }}</Text>
</Stack>
</template>
Examples
Range and unavailable dates
min and max constrain the field with full date times, and the calendar disables dates by their date part; unavailable marks dates as unavailable; minuteStep steps the minute segment in the popover.
<script setup lang="ts">
import { ref } from 'vue'
import { DateTimePicker } from '@hina-ui/vue'
const at = ref<string | null>('2026-09-10T20:00')
const weekend = (value: string) => [0, 6].includes(new Date(`${value}T00:00`).getDay())
</script>
<template>
<DateTimePicker
v-model="at"
min="2026-09-01T00:00"
max="2026-09-30T23:59"
:minute-step="15"
:unavailable="weekend"
aria-label="Event time"
class="w-80"
/>
</template>
Clearable
clearable adds a clear button after the segments that empties the value to null.
<script setup lang="ts">
import { ref } from 'vue'
import { DateTimePicker } from '@hina-ui/vue'
const at = ref<string | null>('2026-09-04T20:00')
</script>
<template>
<DateTimePicker v-model="at" clearable aria-label="Publish time" class="w-80" />
</template>
Sizes
size has three steps, sm, md and lg, matching the input of the same step in height; the calendar, time field and button in the popover use the same step.
<script setup lang="ts">
import { DateTimePicker, Stack } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-80">
<DateTimePicker size="sm" model-value="2026-09-04T20:00" aria-label="Small" />
<DateTimePicker size="md" model-value="2026-09-04T20:00" aria-label="Medium" />
<DateTimePicker size="lg" model-value="2026-09-04T20:00" aria-label="Large" />
</Stack>
</template>
States
invalid gives the field a warning color, disabled disables the group and readonly makes it read only. variant="secondary" is the flat form used inside a surface.
<script setup lang="ts">
import { DateTimePicker, Stack } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-80">
<DateTimePicker model-value="2026-09-04T20:00" invalid aria-label="Invalid" />
<DateTimePicker model-value="2026-09-04T20:00" disabled aria-label="Disabled" />
<DateTimePicker model-value="2026-09-04T20:00" readonly aria-label="Read only" />
<DateTimePicker model-value="2026-09-04T20:00" variant="secondary" aria-label="Secondary" />
</Stack>
</template>
In a form
Inside a FormField the label is linked to the input, and the error message is rendered by the field; validation rules and submission belong to the Form.
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, DateTimePicker, Form, FormField, Text } from '@hina-ui/vue'
const schema = v.object({
startsAt: v.string('Pick a start time'),
})
const values = reactive({ startsAt: 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="startsAt" label="Starts at" required>
<DateTimePicker v-model="values.startsAt" />
</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 the button at the end opens the popover with focus on the current date. Picking a day updates only the date part, keeps the time part and leaves the popover open; with no value yet, the time part comes from
placeholder, or midnight without one. - The time field in the popover edits the time part directly. Clicking “OK” closes the popover and returns focus to the button; clicking anywhere outside or pressing Esc closes it too, since every step is already written back.
- The popover is modal: focus stays inside it while open and the page stops scrolling. Esc inside the time segments does not close the popover; use “OK” or click outside.
- The popover opens below the field, aligned with its left edge.
Accessibility
- See
DateField,CalendarandTimeFieldfor the semantics of the segments, the calendar and the time field; the time field in the popover carries a localized name. - The button that opens the popover carries a localized name and
aria-expanded. - Name the group with
aria-labeloraria-labelledby.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | null | null | YYYY-MM-DDTHH:mm, down to ss at second precision |
open | boolean | false | Whether the popover is open, v-model:open |
placeholder | string | Today | Where the segments and calendar start while empty; its time part is the default time after picking a day |
min | string | — | Lower bound of the accepted range |
max | string | — | Upper bound of the accepted range |
granularity | 'minute' | 'second' | 'minute' | Precision |
hourCycle | 12 | 24 | Locale | Hour cycle |
minuteStep | number | — | Step of the minute segment in the popover |
unavailable | (date: string) => boolean | — | Whether a given date is unavailable |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | Locale | First day of the week |
weekdayFormat | 'narrow' | 'short' | 'narrow' | Format of the weekday names |
fixedWeeks | boolean | true | Whether the calendar always shows six weeks |
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 |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: string | null | The value changed |
update:open | open: boolean | The popover opened or closed |
clear | — | The clear was clicked |