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>
Date
mm
dd
yyyy
<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 keydata-slotElement
basedate-picker<div> root (the field; also a DateField)
—labelLabel (iff label / #label)
groupdate-input-group<div role="group"> holding the segments and the trigger
inputdate-input-group-input<div> wrapping the segments
segmentdate-input-group-segmentone per date part (year, month, day, …)
triggerdate-picker-trigger<button> that opens the popover, rendered in the group's suffix
triggerIndicatordate-picker-trigger-indicator<span> holding the default/custom calendar icon
—descriptionDescription (iff description / #description)
—field-errorFieldError (while invalid, iff error-message / #error / validation errors)
popoverdate-picker-popoverthe popover content wrapping the Calendar

DatePicker combines a segmented date field and a calendar popover into a single accessible component. Values are @internationalized/date objects (CalendarDate, CalendarDateTime or ZonedDateTime) — the same types HeroUI uses.

Examples

Disabled

Date
10
3
2026
This date picker is 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.

Date
10
3
2026
Current value: 2026-10-03
<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.

Appointment date
mm
dd
yyyy
<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.

Date and time
2
3
2026
8
45
AM
PST
<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.

Appointment date
mm
dd
yyyy
Choose a valid appointment date.
<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.

Date
mm
dd
yyyy
Replace the default calendar icon by passing a custom indicator.
<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.

Date
mm
dd
yyyy
<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.

Event date
11
7
1948
शक
<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.

Event date
mm
dd
yyyy
<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 internal date-input-group's slots, shared with DateField/TimeField
  • trigger → [data-slot="date-picker-trigger"] – button that opens the popover
  • triggerIndicator → [data-slot="date-picker-trigger-indicator"] – default/custom calendar icon
  • popover → [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

  • shouldForceLeadingZeros has no equivalent: Reka's DateFieldRoot doesn't expose a flag to force leading zeros on numeric segments the way React Aria's useDateFormatter does, so the format-options demo drops that control.
  • Selecting a calendar day always closes the popover (shouldCloseOnSelect-style behavior is built into Reka's PopoverRoot/Calendar wiring, not configurable), matching HeroUI's own default — this is also why the format-options demo's inline time field only stays reachable before a date is picked.

API Reference

Props

PropTypeDefaultDescription
labelstring-Label text rendered above the group.
calendarLabelstring-Accessible label for the calendar inside the popover. HeroUI's demos set this independently of label.
descriptionstring-Helper text rendered below the group. Hidden while the field is invalid.
errorMessagestring-Error message rendered below the group while invalid. Defaults to the validation errors.
idstringuseId()The id of the field; label, description and error ids derive from it.
namestring-The name of the value, used when submitting a form.
defaultValueDateValue-The initial value when uncontrolled (no v-model).
placeholderDateValue-The placeholder date, used to determine which segments/month to show when no date is selected.
defaultPlaceholderDateValue-The placeholder date when uncontrolled.
granularityGranularity'day' (a `CalendarDate` value), otherwise 'minute'The granularity of the field: which segments are rendered, up to and including this unit.
hourCycleHourCycle-The hour cycle used to format times. Defaults to the locale's preference.
stepDateStep1The stepping interval for the time segments.
stepSnappingbooleanfalseWhether to snap the time value to the nearest step increment after input.
hideTimeZoneboolean-Whether to hide the time zone segment.
minValueDateValue-The minimum selectable date.
maxValueDateValue-The maximum selectable date.
localestring-The locale used to format the field and calendar. Defaults to the ConfigProvider locale.
isDateUnavailableMatcher-A function that returns whether a given date is unavailable for selection.
variant"primary" | "secondary"'primary'Visual variant of the group.
fullWidthbooleanfalseWhether the field takes the full width of its container.
disabledbooleanfalseWhether the picker is disabled.
invalidbooleanundefinedWhether the field is invalid. Overrides validation when set.
requiredbooleanfalseWhether the field is required.
readonlybooleanfalseWhether the value can be selected but not changed.
validateValidateFn<DateValue>-Validates the committed value. Return an error message (or several) when invalid.
validationBehaviorValidationBehavior'native'native blocks form submission and shows errors on commit or submit; aria shows errors in realtime. Defaults to the surrounding Form.
openbooleanundefinedThe controlled open state of the popover.
defaultOpenbooleanfalseThe 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.
modelValueDateValue | null-The selected date. Bind with v-model.

Slots

SlotPropsDescription
labelanyLabel content. Replaces the label prop.
indicatoranyReplaces the default calendar icon inside the trigger.
descriptionanyDescription content. Replaces the description prop.
errorValidationResultError 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

EventPayloadDescription
update:modelValue[value: DateValue | null | undefined]-
update:open[value: boolean]-
update:placeholder[value: DateValue]-

Accessibility

  • The label's for points to the field's id (auto-generated with useId() unless id is set)
  • The field's aria-describedby lists the ids of the rendered description (<id>-description) and error (<id>-error)
  • invalid sets aria-invalid="true" on the field; the error is announced via role="alert"
  • Keyboard: ←/→ move between segments, ↑/↓ increment/decrement the focused segment, digits type directly into the focused segment and auto-advance
  • min-value, max-value and is-date-unavailable mark the field invalid on their own, matching React Aria's useDateFieldState — no need to compute invalid yourself just for native-range feedback
  • The popover's calendar is reachable via the trigger button and closes on Esc or an outside click

On this page

No Headings