DatePicker
Combines a DateField and a Calendar in a popover for picking a date
Usage
<script setup lang="ts">
import { DatePicker } from '@hareui/vue'
</script><script setup lang="ts">import { DatePicker } from '@hareui/vue'</script><template> <DatePicker class="w-72" name="date" label="Date" calendar-label="Event date" /></template>Anatomy
DatePicker renders a Label, a segmented date-input-group holding the field segments and a calendar trigger button, a Description/FieldError, and a popover with an embedded Calendar. The text parts come from props; each can be replaced with a slot.
<template>
<DatePicker label="…" calendar-label="…" description="…" error-message="…" :invalid="…">
<template #label /> <!-- Label content -->
<template #indicator /> <!-- replaces the default calendar icon in the trigger -->
<template #description /> <!-- Description content -->
<template #error /> <!-- FieldError content, rendered when invalid -->
<template #calendar /> <!-- replaces the default Calendar rendered inside the popover -->
</DatePicker>
</template>ui key | data-slot | Element |
|---|---|---|
base | date-picker | <div> root (the field; also a DateField) |
| — | label | Label (iff label / #label) |
group | date-input-group | <div role="group"> holding the segments and the trigger |
input | date-input-group-input | <div> wrapping the segments |
segment | date-input-group-segment | one per date part (year, month, day, …) |
trigger | date-picker-trigger | <button> that opens the popover, rendered in the group's suffix |
triggerIndicator | date-picker-trigger-indicator | <span> holding the default/custom calendar icon |
| — | description | Description (iff description / #description) |
| — | field-error | FieldError (while invalid, iff error-message / #error / validation errors) |
popover | date-picker-popover | the popover content wrapping the Calendar |
DatePicker combines a segmented date field and a calendar popover into a single accessible component. Values are
@internationalized/dateobjects (CalendarDate,CalendarDateTimeorZonedDateTime) — the same types HeroUI uses.
Examples
Disabled
<script setup lang="ts">import { getLocalTimeZone, today } from '@internationalized/date'import { DatePicker } from '@hareui/vue'</script><template> <DatePicker disabled class="w-72" name="date" label="Date" calendar-label="Event date" :model-value="today(getLocalTimeZone())" description="This date picker is disabled." /></template>Controlled
Bind the value with v-model to synchronize with other components or state management.
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { getLocalTimeZone, today } from '@internationalized/date'import { Button, DatePicker, Description } from '@hareui/vue'import { shallowRef } from 'vue'const value = shallowRef<DateValue | null>(today(getLocalTimeZone()))</script><template> <div class="flex w-72 flex-col gap-4"> <DatePicker v-model="value" name="date" label="Date" calendar-label="Event date" /> <Description>Current value: {{ value ? value.toString() : '(empty)' }}</Description> <div class="flex gap-2"> <Button variant="tertiary" @click="value = today(getLocalTimeZone())"> Set today </Button> <Button variant="tertiary" @click="value = null"> Clear </Button> </div> </div></template>Validation
Set invalid with error-message to control the state yourself, or validate with required, min-value/max-value/is-date-unavailable, a validate function, or a surrounding Form.
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { getLocalTimeZone, today } from '@internationalized/date'import { DatePicker } from '@hareui/vue'import { computed, shallowRef } from 'vue'const value = shallowRef<DateValue | null>(null)const todayDate = today(getLocalTimeZone())const isInvalid = computed(() => value.value !== null && value.value.compare(todayDate) < 0)</script><template> <DatePicker v-model="value" required class="w-72" :invalid="isInvalid" :min-value="todayDate" name="date" label="Appointment date" calendar-label="Event date" error-message="Date must be today or in the future." /></template>Format Options
Control how values are displayed with granularity, hour-cycle and hide-time-zone. For granularities beyond "day", the #calendar slot composes a TimeField alongside the Calendar to edit the time portion.
<script setup lang="ts">import type { CalendarDateTime, DateValue, ZonedDateTime } from '@internationalized/date'import type { ListBoxItem, SelectValue } from '@hareui/vue'import type { TimeValue } from 'reka-ui'import { parseDate, parseZonedDateTime, Time } from '@internationalized/date'import { Calendar, DatePicker, Label, Select, Switch, TimeField } from '@hareui/vue'import { computed, ref, shallowRef } from 'vue'type Granularity = 'day' | 'hour' | 'minute' | 'second'type HourCycle = 12 | 24const granularityOptions: ListBoxItem[] = [ { value: 'day', label: 'Day' }, { value: 'hour', label: 'Hour' }, { value: 'minute', label: 'Minute' }, { value: 'second', label: 'Second' },]const hourCycleOptions: ListBoxItem[] = [ { value: 12, label: '12-hour' }, { value: 24, label: '24-hour' },]const granularity = ref<Granularity>('minute')const hourCycle = ref<HourCycle>(12)const hideTimeZone = ref(false)const timeGranularity = computed(() => (granularity.value === 'day' ? undefined : granularity.value))function defaultValueFor(g: Granularity): DateValue { return g === 'day' ? parseDate('2026-02-03') : parseZonedDateTime('2026-02-03T08:45:00[America/Los_Angeles]')}const value = shallowRef<DateValue | null>(defaultValueFor(granularity.value))function hasTime(v: DateValue | null | undefined): v is CalendarDateTime | ZonedDateTime { return !!v && 'hour' in v}// Reka's embedded Calendar only edits date fields and the TimeField below only edits time fields;// each merges onto the other's current value instead of replacing it outright.function mergeDate(picked: DateValue, current: DateValue | null | undefined): DateValue { return hasTime(current) ? current.set({ year: picked.year, month: picked.month, day: picked.day }) : picked}function mergeTime(time: TimeValue, current: DateValue | null | undefined): DateValue | null { return hasTime(current) ? current.set({ hour: time.hour, minute: time.minute, second: time.second }) : (current ?? null)}function onGranularityChange(next: SelectValue | undefined) { if (typeof next !== 'string') return granularity.value = next as Granularity value.value = defaultValueFor(granularity.value)}</script><template> <div class="flex flex-col gap-4"> <DatePicker :key="granularity" v-model="value" class="w-fit min-w-72" :granularity="granularity" :hour-cycle="hourCycle" :hide-time-zone="hideTimeZone" name="date" label="Date and time" calendar-label="Event date" > <template v-if="timeGranularity" #calendar="{ value: calendarValue, onSelect }"> <div class="flex flex-col gap-3"> <Calendar :model-value="calendarValue" year-picker @update:model-value="(picked) => onSelect(mergeDate(picked as DateValue, calendarValue))" /> <div class="flex items-center justify-between"> <Label>Time</Label> <TimeField :model-value="hasTime(calendarValue) ? new Time(calendarValue.hour, calendarValue.minute, calendarValue.second) : undefined" :granularity="timeGranularity" :hour-cycle="hourCycle" :hide-time-zone="hideTimeZone" aria-label="Time" @update:model-value="(time) => time && onSelect(mergeTime(time, calendarValue) as DateValue)" /> </div> </div> </template> </DatePicker> <div class="flex flex-wrap gap-4"> <Select class="w-[120px]" :model-value="granularity" placeholder="Select granularity" variant="secondary" :items="granularityOptions" @update:model-value="onGranularityChange" /> <Select class="w-[120px]" :model-value="hourCycle" placeholder="Select hour cycle" variant="secondary" :items="hourCycleOptions" @update:model-value="(v) => (hourCycle = v as HourCycle)" /> </div> <div class="flex min-w-80 flex-col gap-2"> <Switch v-model="hideTimeZone" label="Hide timezone" /> </div> </div></template>Form Example
Complete form example with validation and submission handling.
<script setup lang="ts">import type { DateValue } from '@internationalized/date'import { getLocalTimeZone, today } from '@internationalized/date'import { Button, DatePicker, Form } from '@hareui/vue'import { computed, ref, shallowRef } from 'vue'const value = shallowRef<DateValue | null>(null)const isSubmitting = ref(false)const todayDate = today(getLocalTimeZone())const isInvalid = computed(() => value.value !== null && value.value.compare(todayDate) < 0)function handleSubmit(e: Event) { e.preventDefault() if (!value.value || isInvalid.value) return isSubmitting.value = true // Simulate API call setTimeout(() => { console.log('Date submitted:', { appointmentDate: value.value }) value.value = null isSubmitting.value = false }, 1200)}</script><template> <Form class="flex w-72 flex-col gap-3" @submit="handleSubmit"> <DatePicker v-model="value" required :invalid="isInvalid" :min-value="todayDate" name="appointmentDate" label="Appointment date" calendar-label="Event date" error-message="Date must be today or in the future." description="Choose a valid appointment date." /> <Button class="w-full" :disabled="!value || isInvalid" :pending="isSubmitting" type="submit"> {{ isSubmitting ? 'Submitting...' : 'Submit' }} </Button> </Form></template>Custom Indicator
The #indicator slot replaces the default calendar icon rendered inside the trigger.
<script setup lang="ts">import { DatePicker } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <DatePicker class="w-72" name="date" label="Date" calendar-label="Event date" description="Replace the default calendar icon by passing a custom indicator."> <template #indicator> <Icon class="size-4" icon="gravity-ui:chevron-down" /> </template> </DatePicker></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 DatePicker fall through to the root element instead.
<script setup lang="ts">import { DatePicker } 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. --> <DatePicker data-custom="date-picker" class="w-72" name="date" label="Date" calendar-label="Event date" /></template>International Calendar
By default, DatePicker displays dates using the calendar system for the given locale. The example below shows the Indian calendar system.
<script setup lang="ts">import { getLocalTimeZone, today } from '@internationalized/date'import { DatePicker } from '@hareui/vue'</script><template> <DatePicker class="w-72" name="international-date" label="Event date" calendar-label="Event date" locale="hi-IN-u-ca-indian" :default-value="today(getLocalTimeZone())" /></template>Note: update:modelValue always emits a date in the same calendar system as model-value or default-value (Gregorian if neither is given), regardless of the displayed locale.
Customization
Tailwind CSS
The #calendar slot lets you fully restyle (or replace) the embedded Calendar; bind its value/onSelect to keep it wired to the picker.
<script setup lang="ts">import { Calendar, DatePicker } from '@hareui/vue'</script><template> <!-- HeroUI's custom-styles demo restyles the embedded Calendar's own sub-parts directly. Option A's DatePicker has no compound parts to reach into the embedded Calendar; the `#calendar` slot replaces it wholesale instead — bind `value` and call `onSelect` to keep it wired to the picker. --> <DatePicker class="w-72" name="event-date" label="Event date" calendar-label="Event date" variant="secondary" :ui="{ group: 'rounded-xl border border-border/80 bg-default shadow-sm' }" > <template #calendar="{ value, onSelect }"> <Calendar class="rounded-2xl bg-surface p-2" :ui="{ cell: 'rounded-lg [&:where([data-selected]):not([data-unavailable])]:bg-accent [&:where([data-selected]):not([data-unavailable])]:text-accent-foreground' }" :model-value="value" @update:model-value="(v) => v && !Array.isArray(v) && onSelect(v)" /> </template> </DatePicker></template>Global Configuration
app.use(createHareUI({
ui: { datePicker: { slots: { trigger: 'text-muted', popover: 'p-0' } } },
}))Styling Reference
Slots
base→[data-slot="date-picker"]– root field container (inline-flex flex-col gap-1)group,input,segment→ the internaldate-input-group's slots, shared with DateField/TimeFieldtrigger→[data-slot="date-picker-trigger"]– button that opens the popovertriggerIndicator→[data-slot="date-picker-trigger-indicator"]– default/custom calendar iconpopover→[data-slot="date-picker-popover"]– popover content wrapping the Calendar
Note: Label, Description, FieldError and Calendar have their own themes. See their respective pages for customization options.
Interactive States
DatePicker sets these data attributes:
- Invalid:
[data-invalid="true"]on the field and the group — automatically hides the description while invalid - Required:
[data-required="true"]on the field — shows the label's required asterisk - Disabled:
[data-disabled="true"]on the field,[data-disabled]on the group, each segment and the trigger - Open:
[data-state="open"|"closed"]on the trigger and the popover - Focus Visible:
[data-focus-visible="true"]on the trigger
Reka UI Limitations
shouldForceLeadingZeroshas no equivalent: Reka'sDateFieldRootdoesn't expose a flag to force leading zeros on numeric segments the way React Aria'suseDateFormatterdoes, so theformat-optionsdemo drops that control.- Selecting a calendar day always closes the popover (
shouldCloseOnSelect-style behavior is built into Reka'sPopoverRoot/Calendarwiring, not configurable), matching HeroUI's own default — this is also why theformat-optionsdemo's inline time field only stays reachable before a date is picked.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Label text rendered above the group. |
calendarLabel | string | - | Accessible label for the calendar inside the popover. HeroUI's demos set this independently of label. |
description | string | - | Helper text rendered below the group. Hidden while the field is invalid. |
errorMessage | string | - | Error message rendered below the group while invalid. Defaults to the validation errors. |
id | string | useId() | The id of the field; label, description and error ids derive from it. |
name | string | - | The name of the value, used when submitting a form. |
defaultValue | DateValue | - | The initial value when uncontrolled (no v-model). |
placeholder | DateValue | - | The placeholder date, used to determine which segments/month to show when no date is selected. |
defaultPlaceholder | DateValue | - | The placeholder date when uncontrolled. |
granularity | Granularity | 'day' (a `CalendarDate` value), otherwise 'minute' | The granularity of the field: which segments are rendered, up to and including this unit. |
hourCycle | HourCycle | - | The hour cycle used to format times. Defaults to the locale's preference. |
step | DateStep | 1 | The stepping interval for the time segments. |
stepSnapping | boolean | false | Whether to snap the time value to the nearest step increment after input. |
hideTimeZone | boolean | - | Whether to hide the time zone segment. |
minValue | DateValue | - | The minimum selectable date. |
maxValue | DateValue | - | The maximum selectable date. |
locale | string | - | The locale used to format the field and calendar. Defaults to the ConfigProvider locale. |
isDateUnavailable | Matcher | - | A function that returns whether a given date is unavailable for selection. |
variant | "primary" | "secondary" | 'primary' | Visual variant of the group. |
fullWidth | boolean | false | Whether the field takes the full width of its container. |
disabled | boolean | false | Whether the picker is disabled. |
invalid | boolean | undefined | Whether the field is invalid. Overrides validation when set. |
required | boolean | false | Whether the field is required. |
readonly | boolean | false | Whether the value can be selected but not changed. |
validate | ValidateFn<DateValue> | - | Validates the committed value. Return an error message (or several) when invalid. |
validationBehavior | ValidationBehavior | 'native' | native blocks form submission and shows errors on commit or submit; aria shows errors in realtime.
Defaults to the surrounding Form. |
open | boolean | undefined | The controlled open state of the popover. |
defaultOpen | boolean | false | The open state of the popover when it is initially rendered. |
ui | (ComponentSlots<{ slots: { base: string; trigger: string[]; triggerIndicator: string; popover: 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" | "segment">) | - | Per-slot class overrides. group, input and segment are the internal date-input-group's. |
modelValue | DateValue | null | - | The selected date. Bind with v-model. |
Slots
| Slot | Props | Description |
|---|---|---|
label | any | Label content. Replaces the label prop. |
indicator | any | Replaces the default calendar icon inside the trigger. |
description | any | Description content. Replaces the description prop. |
error | ValidationResult | Error content, rendered while invalid. Replaces the errorMessage prop. |
calendar | { value: DateValue | null | undefined; onSelect: (value: DateValue) => void; } | Replaces the default Calendar rendered inside the popover. Bind value and call onSelect
on your own Calendar to keep it wired to the picker (see the custom-styles demo). |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | [value: DateValue | null | undefined] | - |
update:open | [value: boolean] | - |
update:placeholder | [value: DateValue] | - |
Accessibility
- The label's
forpoints to the field's id (auto-generated withuseId()unlessidis set) - The field's
aria-describedbylists the ids of the rendered description (<id>-description) and error (<id>-error) invalidsetsaria-invalid="true"on the field; the error is announced viarole="alert"- Keyboard: ←/→ move between segments, ↑/↓ increment/decrement the focused segment, digits type directly into the focused segment and auto-advance
min-value,max-valueandis-date-unavailablemark the field invalid on their own, matching React Aria'suseDateFieldState— no need to computeinvalidyourself just for native-range feedback- The popover's calendar is reachable via the trigger button and closes on Esc or an outside click









