Type a date or choose one from a calendar, with support for ranges and forms.

Date Picker combines a text field with Calendar. People can type a date in their language or choose it from the popup. Your application receives an ISO date string either way.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
</script>

<template>
  <DatePicker default-value="2026-10-16">
    <div data-slot="row">
      <DatePickerInput aria-label="Due date" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerHint />
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
    </DatePickerContent>
  </DatePicker>
</template>
Type a date and press Enter, or open the calendar and choose a day. The field formats the date when you commit it.

Installation

npm install @ui-sdk/vue @ui-sdk/core

Composition

Use the following composition to build a DatePicker:

DatePicker
โ”œโ”€โ”€ DatePickerInput
โ”œโ”€โ”€ DatePickerTrigger
โ”œโ”€โ”€ DatePickerHint
โ””โ”€โ”€ DatePickerContent

The input accepts a date, the trigger opens the calendar, and the hint describes the expected format. An empty content part renders a calendar for you.

Every Calendar part also works inside DatePickerContent and is exported from the Date Picker entry. Write those parts out when you need to add styles, change the layout, or include custom day content.

An empty DatePicker renders the input, trigger, and calendar too. In range mode, it renders an input for each end.

Usage

Import the parts and put them together:

<script setup lang="ts">
import {
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
</script>

<template>
  <DatePicker default-value="2026-10-16">
    <DatePickerInput aria-label="Due date" />
    <DatePickerTrigger>Choose date</DatePickerTrigger>
    <DatePickerHint />
    <DatePickerContent />
  </DatePicker>
</template>

Values

Dates are strings such as "2026-10-16", with no time or time zone attached. No date library is needed.

ModeValue
"single" (default)A date string or null.
"range"[start, end] or null, with an input for each end.

Typing a Date

The input parses text when you press Enter or leave the field. It uses the localeโ€™s date order, such as 10/16/2026 in American English or 16.10.2026 in German. It also accepts ISO dates, digits without separators, two-digit years, and month names in the locale or English. Dates without a year use the current year.

The field formats a valid date after it is committed, leaving your typing alone until then. Invalid text stays visible and sets aria-invalid and data-invalid; the previous value stays unchanged. Until the text is a date again, or empty, the input reports "Enter a valid date." as its validation message, so its form won't submit. The hintโ€™s state includes an example date you can show alongside the field.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
</script>

<template>
  <DatePicker default-value="2026-10-16">
    <div data-slot="row">
      <DatePickerInput aria-label="Due date" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
    </DatePickerContent>
    <DatePickerHint v-slot="{ state: hint }">
      Like {{ hint.example }}
    </DatePickerHint>
  </DatePicker>
</template>
Type not a date and press Enter to mark the field invalid. Replace it with 10/18/2026 to clear the invalid state.

Range

mode="range" uses one input for the start and another with end for the end. The calendar stays open after the start is picked and closes once both dates are chosen. A typed end before the start is invalid.

The range is null until both ends are valid. Clearing either input clears the complete value while leaving the other inputโ€™s date visible.

For two months side by side, set :months="2" and add a second CalendarGrid with :offset="1". page-by="page" moves both months together.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
import { ref } from "vue"

const field = ref<HTMLElement | null>(null)
</script>

<template>
  <DatePicker
    mode="range"
    :months="2"
    month-format="short"
    :default-value="['2026-10-12', '2026-10-16']"
  >
    <div ref="field" data-slot="row">
      <DatePickerInput aria-label="Check in" placeholder="Check in" />
      <span data-slot="hint">โ€“</span>
      <DatePickerInput end aria-label="Check out" placeholder="Check out" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerContent :anchor="field" align="center">
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <div data-slot="row">
        <CalendarGrid>
          <CalendarWeekdays v-slot="{ index }">
            <CalendarWeekday :index="index" />
          </CalendarWeekdays>
          <CalendarWeeks v-slot="{ cell }">
            <CalendarDay :value="cell.date" />
          </CalendarWeeks>
        </CalendarGrid>
        <CalendarGrid :offset="1">
          <CalendarWeekdays v-slot="{ index }">
            <CalendarWeekday :index="index" />
          </CalendarWeekdays>
          <CalendarWeeks v-slot="{ cell }">
            <CalendarDay :value="cell.date" />
          </CalendarWeeks>
        </CalendarGrid>
      </div>
    </DatePickerContent>
  </DatePicker>
</template>
Choose October 20, then October 23. The calendar stays open after the first date and closes after the second.

Presets

DatePickerPreset chooses its value and closes the popup. It exposes aria-pressed and data-selected when that value matches the selection. Your application calculates relative dates, such as the last seven days.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerInput,
  DatePickerPreset,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
</script>

<template>
  <DatePicker mode="range" today="2026-10-16">
    <div data-slot="row">
      <DatePickerInput aria-label="Check in" placeholder="Check in" />
      <span data-slot="hint">โ€“</span>
      <DatePickerInput end aria-label="Check out" placeholder="Check out" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
      <div data-slot="row">
        <DatePickerPreset
          data-variant="ghost"
          :value="['2026-10-10', '2026-10-16']"
        >
          Last 7 Days
        </DatePickerPreset>
        <DatePickerPreset
          data-variant="ghost"
          :value="['2026-09-17', '2026-10-16']"
        >
          Last 30 Days
        </DatePickerPreset>
        <DatePickerPreset
          data-variant="ghost"
          :value="['2026-10-01', '2026-10-31']"
        >
          This Month
        </DatePickerPreset>
      </div>
    </DatePickerContent>
  </DatePicker>
</template>
Open the calendar and choose Last 7 Days. Reopen it to see the selected preset.

Min, Max and Unavailable Dates

min, max, and unavailable apply to typing, calendar selection, presets, and the controller. A date outside those rules is invalid when typed, a preset that holds one is disabled, and the controller refuses it. The controller also refuses a value while the picker is disabled or read-only. Calendar explains the available matchers and how unavailable dates differ from the bounds.

For ranges, min-days and max-days count both endpoints. allow-unavailable permits unavailable dates between them, but never as an endpoint. See Range Limits.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
</script>

<template>
  <DatePicker
    min="2026-10-05"
    max="2026-11-20"
    :unavailable="[{ weekday: [0, 6] }]"
    default-value="2026-10-16"
  >
    <div data-slot="row">
      <DatePickerInput aria-label="Delivery date" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerHint />
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
    </DatePickerContent>
  </DatePicker>
</template>
Try typing 11/21/2026, past the last allowed date, then 10/17/2026, a Saturday. Both are invalid and can't be picked; 10/19/2026 is accepted.

Language

locale sets the inputโ€™s format, hint, month names, and date labels. If you omit it, Date Picker reads the surrounding pageโ€™s lang after it mounts, with the browserโ€™s language as a fallback. The initial server and client render use English, so they always match. Date strings remain Gregorian ISO dates.

The trigger, popup, and calendar navigation have English accessible labels by default. Pass aria-label to translate those controls too.

Calendarโ€™s week-starts-on, weekday-format, fixed-weeks, show-outside-days, and time-zone options apply here too. Calendar shows these options without the popup.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
</script>

<template>
  <div lang="de">
    <DatePicker default-value="2026-10-16">
      <div data-slot="row">
        <DatePickerInput aria-label="Fรคlligkeitsdatum" />
        <DatePickerTrigger data-variant="ghost">
          <CalendarIcon aria-hidden="true" />
        </DatePickerTrigger>
      </div>
      <DatePickerHint />
      <DatePickerContent>
        <div data-slot="row">
          <CalendarPrevious data-variant="ghost">
            <ChevronLeftIcon aria-hidden="true" />
          </CalendarPrevious>
          <CalendarHeading />
          <CalendarNext data-variant="ghost">
            <ChevronRightIcon aria-hidden="true" />
          </CalendarNext>
        </div>
        <CalendarGrid>
          <CalendarWeekdays v-slot="{ index }">
            <CalendarWeekday :index="index" />
          </CalendarWeekdays>
          <CalendarWeeks v-slot="{ cell }">
            <CalendarDay :value="cell.date" />
          </CalendarWeeks>
        </CalendarGrid>
      </DatePickerContent>
    </DatePicker>
  </div>
</template>
Enter 3.12.2026 and press Enter. The field, format hint, and calendar use German dates.

Clear

DatePickerClear clears the value and focuses the input. It gets data-empty when there is nothing to clear. Emptying a single-date input clears its value too.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerClear,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import {
  CalendarIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  XIcon,
} from "@lucide/vue"
</script>

<template>
  <DatePicker default-value="2026-10-16">
    <div data-slot="row">
      <DatePickerInput aria-label="Due date" />
      <DatePickerClear data-variant="ghost" aria-label="Clear date">
        <XIcon aria-hidden="true" />
      </DatePickerClear>
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerHint />
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
    </DatePickerContent>
  </DatePicker>
</template>
Press Clear to empty the value and focus the input. The Clear button disappears until there is a value again.

Disabled and Read Only

disabled prevents typing, opening, and selection. read-only lets people open the calendar and inspect its dates without changing the value.

<DatePicker disabled default-value="2026-10-16" />
<DatePicker read-only default-value="2026-10-16" />

Staying Open

Set close-on-select to false to keep the calendar open after choosing a date, completing a range, or pressing a preset.

<DatePicker :close-on-select="false" />

Keep It Open Until Both Dates Are Picked

Set dismissible to false while a choice is incomplete. Escape, an outside press, or the system back gesture then leave the popup open. @dismiss-attempt reports intentional attempts, so you can explain what is missing. Focus leaving is refused quietly. The trigger and imperative controls can still close it.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerInput,
  DatePickerTrigger,
  type DatePickerValue,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
import { ref } from "vue"

const value = ref<DatePickerValue>(null)
const asking = ref(false)
</script>

<template>
  <DatePicker
    v-model:value="value"
    mode="range"
    default-month="2026-10"
    :dismissible="value !== null"
    @value-change="asking = false"
    @open-change="(open: boolean) => open && (asking = false)"
    @dismiss-attempt="asking = true"
  >
    <div data-slot="row">
      <DatePickerInput aria-label="Check in" placeholder="Check in" />
      <span data-slot="hint">โ€“</span>
      <DatePickerInput end aria-label="Check out" placeholder="Check out" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
      <p v-if="asking" data-slot="hint" role="alert">
        Pick a check-out date to finish your stay.
      </p>
    </DatePickerContent>
  </DatePicker>
</template>
Choose a check-in date, then press outside the calendar. The message asks for a check-out date before the popup can be dismissed.

DatePickerContent opens below the input by default. Position it with side, align, and offsets, or provide a custom anchor. container changes where it renders; keep-mounted retains it while closed, and final-focus controls focus after it closes. Popups covers these shared options.

presentation="auto" uses a sheet on small touch screens and a popover elsewhere. presentation="sheet" always uses a sheet. Style the two layouts using data-presentation.

Console
<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
  type DatePickerMonthChangeReason,
  type DatePickerOpenChangeReason,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
import { ref } from "vue"
import { useLog } from "./log"

const log = useLog()
const anchor = ref<HTMLElement | null>(null)
const container = ref<HTMLElement | null>(null)

function onOpenChange(
  open: boolean,
  details: { reason: DatePickerOpenChangeReason }
) {
  log(`${open ? "Opened" : "Closed"} (${details.reason})`)
}

function onOpenChangeComplete(open: boolean) {
  if (!open) {
    log("Finished closing")
  }
}

function onMonthChange(
  month: string,
  details: { reason: DatePickerMonthChangeReason }
) {
  log(`Showing ${month} (${details.reason})`)
}
</script>

<template>
  <div data-slot="stack">
    <DatePicker
      :default-open="false"
      presentation="auto"
      :container="container"
      @open-change="onOpenChange"
      @open-change-complete="onOpenChangeComplete"
      @month-change="onMonthChange"
    >
      <div ref="anchor" data-slot="row">
        <DatePickerInput aria-label="Due date" />
        <DatePickerTrigger data-variant="ghost">
          <CalendarIcon aria-hidden="true" />
        </DatePickerTrigger>
      </div>
      <DatePickerHint />
      <DatePickerContent
        :anchor="anchor"
        :side-offset="6"
        :collision-padding="16"
        keep-mounted
      >
        <div data-slot="row">
          <CalendarPrevious data-variant="ghost">
            <ChevronLeftIcon aria-hidden="true" />
          </CalendarPrevious>
          <CalendarHeading />
          <CalendarNext data-variant="ghost">
            <ChevronRightIcon aria-hidden="true" />
          </CalendarNext>
        </div>
        <CalendarGrid>
          <CalendarWeekdays v-slot="{ index }">
            <CalendarWeekday :index="index" />
          </CalendarWeekdays>
          <CalendarWeeks v-slot="{ cell }">
            <CalendarDay :value="cell.date" />
          </CalendarWeeks>
        </CalendarGrid>
      </DatePickerContent>
    </DatePicker>
    <div ref="container" />
  </div>
</template>
Open the calendar. On a small touch screen it opens as a sheet, and the console logs each open, close and month change.

Forms

name submits an ISO date. In range mode, use start-name and end-name for separate fields, or name to submit both ends under the same name. required blocks an empty submission, text that isn't a date blocks one too, a form reset restores default-value, and a disabled fieldset disables the picker. Server-rendered hidden inputs submit the initial value before hydration too.

In a Field, the field connects the label, description, and errors to the input, and shows an error for an empty required date or text that isn't a date. Its name is used when the picker has none, and a disabled field disables the picker.

Console
<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@ui-sdk/vue/field"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
import { useLog } from "./log"

const log = useLog()

function submit(event: Event) {
  log(
    `Submitted due=${new FormData(event.currentTarget as HTMLFormElement).get("due")}`
  )
}
</script>

<template>
  <form data-slot="stack" @submit.prevent="submit">
    <Field name="due">
      <FieldLabel>Due date</FieldLabel>
      <DatePicker required>
        <div data-slot="row">
          <DatePickerInput />
          <DatePickerTrigger data-variant="ghost">
            <CalendarIcon aria-hidden="true" />
          </DatePickerTrigger>
        </div>
        <DatePickerContent>
          <div data-slot="row">
            <CalendarPrevious data-variant="ghost">
              <ChevronLeftIcon aria-hidden="true" />
            </CalendarPrevious>
            <CalendarHeading />
            <CalendarNext data-variant="ghost">
              <ChevronRightIcon aria-hidden="true" />
            </CalendarNext>
          </div>
          <CalendarGrid>
            <CalendarWeekdays v-slot="{ index }">
              <CalendarWeekday :index="index" />
            </CalendarWeekdays>
            <CalendarWeeks v-slot="{ cell }">
              <CalendarDay :value="cell.date" />
            </CalendarWeeks>
          </CalendarGrid>
        </DatePickerContent>
      </DatePicker>
      <FieldDescription>When the invoice is due.</FieldDescription>
      <div>
        <FieldError match="valueMissing">Choose a due date.</FieldError>
      </div>
    </Field>
    <button type="submit">Submit</button>
  </form>
</template>
Press Submit with an empty field to see the inline error, then choose a date and submit. The console logs the ISO date.

Controlled

The selected value, displayed month, and open state are separate. value uses the shape for the chosen mode; month is the first displayed month, such as "2026-10"; open is a boolean.

Use v-model:value, v-model:month, and v-model:open to control them, and @value-change, @month-change, and @open-change to see each change. default-value, default-month, and default-open set initial state instead. Change details carry a reason; details.cancel() rejects the requested change. State and Events covers the binding pattern, and the reason tables list Date Pickerโ€™s reasons.

Console
<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
  type DatePickerValue,
  type DatePickerValueChangeReason,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"
import { ref } from "vue"
import { useLog } from "./log"

const value = ref<DatePickerValue>("2026-10-16")
const log = useLog()

function onValueChange(
  next: DatePickerValue,
  details: { reason: DatePickerValueChangeReason }
) {
  log(`${next ?? "No date"} (${details.reason})`)
}
</script>

<template>
  <DatePicker v-model:value="value" @value-change="onValueChange">
    <div data-slot="row">
      <DatePickerInput aria-label="Due date" />
      <DatePickerTrigger data-variant="ghost">
        <CalendarIcon aria-hidden="true" />
      </DatePickerTrigger>
    </div>
    <DatePickerHint />
    <DatePickerContent>
      <div data-slot="row">
        <CalendarPrevious data-variant="ghost">
          <ChevronLeftIcon aria-hidden="true" />
        </CalendarPrevious>
        <CalendarHeading />
        <CalendarNext data-variant="ghost">
          <ChevronRightIcon aria-hidden="true" />
        </CalendarNext>
      </div>
      <CalendarGrid>
        <CalendarWeekdays v-slot="{ index }">
          <CalendarWeekday :index="index" />
        </CalendarWeekdays>
        <CalendarWeeks v-slot="{ cell }">
          <CalendarDay :value="cell.date" />
        </CalendarWeeks>
      </CalendarGrid>
    </DatePickerContent>
  </DatePicker>
</template>
Type or choose a date. The console logs each value and the reason for the change.

Driving It from Anywhere

A createDatePickerController() instance can set the value, show a month, open or close the popup, and focus a date in the open calendar. Controllers covers setup and lifecycle; the controller reference lists the methods.

<script setup lang="ts">
import {
  CalendarDay,
  CalendarGrid,
  CalendarHeading,
  CalendarNext,
  CalendarPrevious,
  CalendarWeekday,
  CalendarWeekdays,
  CalendarWeeks,
  createDatePickerController,
  DatePicker,
  DatePickerContent,
  DatePickerHint,
  DatePickerInput,
  DatePickerTrigger,
} from "@ui-sdk/vue/date-picker"
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "@lucide/vue"

const controller = createDatePickerController()
</script>

<template>
  <div data-slot="stack">
    <div data-slot="controls">
      <button type="button" @click="controller.setValue('2026-12-25')">
        Choose Christmas
      </button>
      <button type="button" @click="controller.open()">Open</button>
    </div>
    <DatePicker :controller="controller" default-value="2026-10-16">
      <div data-slot="row">
        <DatePickerInput aria-label="Due date" />
        <DatePickerTrigger data-variant="ghost">
          <CalendarIcon aria-hidden="true" />
        </DatePickerTrigger>
      </div>
      <DatePickerHint />
      <DatePickerContent>
        <div data-slot="row">
          <CalendarPrevious data-variant="ghost">
            <ChevronLeftIcon aria-hidden="true" />
          </CalendarPrevious>
          <CalendarHeading />
          <CalendarNext data-variant="ghost">
            <ChevronRightIcon aria-hidden="true" />
          </CalendarNext>
        </div>
        <CalendarGrid>
          <CalendarWeekdays v-slot="{ index }">
            <CalendarWeekday :index="index" />
          </CalendarWeekdays>
          <CalendarWeeks v-slot="{ cell }">
            <CalendarDay :value="cell.date" />
          </CalendarWeeks>
        </CalendarGrid>
      </DatePickerContent>
    </DatePicker>
  </div>
</template>
Press Choose Christmas to set the date from outside, or Open to open the calendar.

Styling

The root, inputs, and trigger expose data-disabled. Invalid input sets data-invalid on that input and the root. The root and trigger expose data-open; presets expose data-selected, and Clear exposes data-empty. Calendar days carry Calendarโ€™s state attributes.

Each part has a data-slot, such as date-picker-input, and its state attributes are listed under Data Attributes.

DatePickerContent

VariableWhat it holds
--anchor-widthWidth of the anchor, usually the trigger. Use it to match widths.
--anchor-heightHeight of the anchor.
--available-widthSpace beside the popup, up to the viewport edge. Not inherited either.
--available-heightSpace between the anchor and the viewport edge. Use it for max-height. Not inherited, so a child that needs it copies it on the popup, like [--list-height:var(--available-height)], and reads max-h-(--list-height). The first popup registers both names for the whole page with CSS.registerProperty, so they don't inherit anywhere. A page that needs them to inherit calls it first, with inherits: true. An @property rule can't, since a registration in script wins.
--transform-originThe point on the anchor the popup opens from. Use it to scale from the right place.
--arrow-offsetWhere the anchor's center falls along the popup's edge, kept arrowPadding from the corners. Position a ::before arrow with it.
--drawer-progressWhile it presents as a sheet: how far open the sheet is, from 0 closed to 1 at its largest snap point. Never past 1.
--drawer-dimWhile it presents as a sheet: how dim the page behind is, from 0 to 1. The backdrop's opacity follows it.
--drawer-expandWhile it presents as a sheet: 0 at the second-largest snap point and below, 1 at the largest. With one snap point, the whole travel.
--drawer-stackWhile it presents as a sheet: how many sheets opened from inside this one are open above it, rising smoothly as each one opens. Push it back with it.
--sheet-backdropSet it yourself: the color the page dims to while it presents as a sheet. Black at 0.2 by default.

Viewport

VariableWhat it holds
--keyboard-insetWhile it presents as a sheet: how many pixels of the page the on-screen keyboard covers, 0 without one.
--visual-viewport-widthWhile it presents as a sheet: the width of the part of the page you can see.
--visual-viewport-heightWhile it presents as a sheet: the height of the part of the page you can see, above the keyboard.
--visual-viewport-topWhile it presents as a sheet: how far the part of the page you can see is scrolled from the top of the layout, as when the keyboard pushes it up.

Data Attributes

DatePicker

[data-slot="date-picker"]

AttributeWhat it means
data-modesinglerangeHow the date picker selects.
data-openPresent while the popup is open.
data-closedPresent while the popup is closed, including while it animates out.
data-readonlyPresent when the date can be seen but not changed.
data-disabledPresent when the date picker is disabled.
data-invalidPresent when an input holds text that isn't a date, or a date that can't be picked.

DatePickerInput

[data-slot="date-picker-input"]

AttributeWhat it means
data-disabledPresent when the date picker is disabled.
data-invalidPresent when an input holds text that isn't a date, or a date that can't be picked.

DatePickerTrigger

[data-slot="date-picker-trigger"]

AttributeWhat it means
data-openPresent while the popup is open.
data-closedPresent while the popup is closed, including while it animates out.
data-disabledPresent when the date picker is disabled.

DatePickerContent

[data-slot="date-picker-content"]

AttributeWhat it means
data-openPresent while the popup is open.
data-presentationpopoversheetHow the popup presents this time it's open.
data-closedPresent while the popup is closed, including while it animates out.
data-starting-stylePresent for the first frame. Style the state you animate from.
data-ending-stylePresent while the popup animates out. Style the state you animate to.

DatePickerClear

[data-slot="date-picker-clear"]

AttributeWhat it means
data-emptyPresent when there's no date to clear.

DatePickerPreset

[data-slot="date-picker-preset"]

AttributeWhat it means
data-selectedPresent while the preset's value is the one selected.

API Reference

Props are the same as below, and callbacks are events.

DatePicker

debug?boolean
Logs every state change with its reason to the console, in development builds.
month?string
The first month shown, like "2026-10". Pass it to control the calendar.
defaultMonth?string
The first month it shows, when uncontrolled. Defaults to the selected date's month, else today's.
min?string
The earliest date that can be picked. Keys stop there, and Previous stops at its month.
max?string
The latest date that can be picked.
unavailable?DateMatchers
Dates that can't be picked, like booked days, as data: ISO dates, { from, to }, { weekday: [0, 6] } for weekends, { before }, { after }, or a function. They stay focusable, so keyboard users hear them.
disabled?boolean
Whether nothing can be picked or focused.
readOnly?boolean
Whether dates can be focused but not picked.
months?number
How many months show side by side, each in its own grid.
pageBy?"month" | "page"
Whether Previous, Next and keys turn one month, or all the months shown.
locale?string
The language of the names and labels. Defaults to the page's lang, then the browser's.
weekStartsOn?number
The first day of the week, 0 for Sunday. Defaults to the locale's.
weekdayFormat?"long" | "narrow" | "short"
How weekday headers are written.
monthFormat?"long" | "narrow" | "short"
How month names are written, in the heading and the month select.
fixedWeeks?boolean
Whether every month shows six weeks, so the calendar doesn't change height.
showOutsideDays?boolean
Whether the days of the months before and after show at the edges.
today?string
Today's date, for aria-current and the starting month. Defaults to today in timeZone, read on the client.
timeZone?string
The time zone that says what day today is, like "America/New_York". Defaults to the browser's.
minDays?number
The fewest days a range can span, counting both ends.
maxDays?number
The most days a range can span, counting both ends.
allowUnavailable?boolean
Whether a range may hold unavailable dates, like a stay over a booked night.
container?DocumentFragment | Element | null
Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
mode?DatePickerMode
How many dates it selects: one, or a range from a start to an end, with an input for each.
value?DatePickerValue
The selected value: an ISO date, [start, end] in range mode, or null. Pass it to control the date picker.
defaultValue?DatePickerValue
The value it starts with, when uncontrolled, and goes back to when its form resets.
@value-change((value: DatePickerValue, details: ChangeDetails<DatePickerValueChangeReason>) => void)
Runs when a date is picked in the calendar ("item-press"), a typed date is committed on Enter or blur ("input-commit"), a preset or Clear is pressed, the form resets, or a controller sets it. A range changes once both ends are set. details.cancel() keeps the value.
@month-change((month: ISOMonth, details: ChangeDetails<DatePickerMonthChangeReason>) => void)
Runs when the months the calendar shows want to change, as in Calendar, or a typed date or a preset turns them.
open?boolean
Whether the popup is open. Pass it to control the popup.
defaultOpen?boolean
Whether the popup starts open, when uncontrolled.
@open-change((open: boolean, details: ChangeDetails<DatePickerOpenChangeReason>) => void)
Runs when the popup wants to open or close. details.reason says why, and details.cancel() stops it.
@open-change-complete((open: boolean) => void)
Runs once the popup has finished opening or closing, after any animation.
closeOnSelect?boolean
Whether picking a date, or a range's end, or pressing a preset closes the popup.
presentation?PresentationSetting<"popover">
How the popup presents. "sheet" slides it up from the bottom of the screen, and "auto" does that on small touch screens.
name?string
The name the date submits under, as an ISO date. In range mode, both ends submit under it unless startName and endName are set.
startName?string
The name a range's start submits under.
endName?string
The name a range's end submits under.
form?string
The id of the form it belongs to, when it isn't inside it.
required?boolean
Whether a date must be entered before its form submits.
controller?object
Drives it from anywhere. Create one with createDatePickerController().
dismissible?boolean
Whether Escape, an outside press, focus or the window leaving, Android's back gesture and a sheet's swipe can close it. Triggers, Close parts and the controller always can. Pass false while it holds unsaved work, and ask in @dismiss-attempt.
@dismiss-attemptfunction
Runs when dismissible refused a dismissal made on purpose: Escape, an outside press, back or a swipe. Focus or the window leaving is refused without it. Ask "Discard changes?" here.

DatePickerInput

end?boolean
Whether it's a range's end. The input without it holds the start, or the date.

DatePickerTrigger

Opens and closes the calendar. Renders a button with aria-haspopup="dialog", named by the chosen date.

DatePickerHint

Describes the expected format, and its state includes an example date. Renders a span that describes the input.

DatePickerClear

Clears the value and focuses the input. Renders a button.

DatePickerContent

side?Side
Which side of the anchor the popup opens on. Flips to the other side when there isn't room. Which side of the input the popup opens on.
align?Align
How the popup lines up with the anchor along that side. start and end follow the reading direction. How the popup lines up with the input along that side.
sideOffset?number
Gap between the anchor and the popup, in pixels. Gap between the input and the popup, in pixels.
alignOffset?number
Shift along the anchor, in pixels. Shift along the input, in pixels.
collisionPadding?number
Space to keep from the viewport edges, in pixels.
arrowPadding?number
Space to keep between the arrow and the popup's corners, in pixels.
anchor?string | Anchor | null
Positions the popup against this instead of its trigger: an element, a CSS selector, a point like { x, y }, or anything with getBoundingClientRect(). It's read on every check, so it can move. A controller's open({ anchor }) wins for that open.
container?DocumentFragment | Element | null
Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
finalFocus?PopupFocusTarget
Where focus goes when the popup closes while focus is inside it.
keepMounted?boolean
Whether the popup stays in the page, hidden, while closed. For animation libraries that drive the exit from state.

DatePickerPreset

valueDatePickerValue
The value it selects: an ISO date, or [start, end] in range mode.

DatePickerTrigger, DatePickerHint and DatePickerClear take no props of their own.

State

What render functions, state-based class names and children functions receive, per part.

DatePicker:

valueDatePickerValue
The selected value.
modeDatePickerMode
How it selects.
openboolean
Whether the popup is open.
disabledboolean
Whether it can't be used.
readOnlyboolean
Whether dates can be seen but not changed.
invalidboolean
Whether an input holds text that isn't a date that can be picked.

DatePickerInput:

invalidboolean
Whether its text isn't a date that can be picked, after Enter or blur.
disabledboolean
Whether it can't be used.
readOnlyboolean
Whether its text can't be changed.

DatePickerTrigger:

openboolean
Whether the popup is open.
disabledboolean
Whether it can't be pressed.

DatePickerContent:

openboolean
Whether the popup is open. False while it animates out.
presentation"popover" | "sheet"
How it presents this time it's open.

DatePickerHint:

placeholderstring
The format, with a letter for each digit, like "MM/DD/YYYY", "TT.MM.JJJJ" in German.
examplestring
December 31 of this year in the format, like "12/31/2026".

DatePickerPreset:

selectedboolean
Whether its value is the one selected.
disabledboolean
Whether it can't be pressed.

DatePickerClear:

emptyboolean
Whether there's nothing to clear.
disabledboolean
Whether it can't be pressed.

Reasons

details.reason says why the value changed:

item-pressMouseEvent | KeyboardEvent
A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
input-commitKeyboardEvent | FocusEvent
Enter, or leaving a number input, committed what was typed: a number input parses, clamps and snaps it to the step, and an autocomplete closes its suggestions on it.
preset-press
clear-pressMouseEvent | KeyboardEvent
A Clear part was pressed, like on an input, a select or a combobox.
resetnone
A form reset.
restorenone
The browser restored state, like after going back, or a saved value was read back from storage.
imperativenone
A controller or api call, or a property set on a web component.

Why the popup opened or closed:

item-pressMouseEvent | KeyboardEvent
A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
preset-press
imperativenone
A controller or api call, or a property set on a web component.
trigger-pressMouseEvent | KeyboardEvent | TouchEvent
Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its touchend.
outside-pressMouseEvent
A press outside the layer.
escape-keyKeyboardEvent
Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
focus-outKeyboardEvent | FocusEvent
Focus left the layer, or Tab moved it out.
window-blurFocusEvent
The window lost focus.
close-pressMouseEvent | KeyboardEvent | SubmitEvent
A Close part was pressed, or a form with method="dialog" was submitted.
backEvent
Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
swipePointerEvent | TouchEvent
A swipe gesture: a finger, mouse or pen dragged it.

With dismissible false, the refused dismissals @dismiss-attempt reports:

outside-pressMouseEvent
A press outside the layer.
escape-keyKeyboardEvent
Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
backEvent
Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
swipePointerEvent | TouchEvent
A swipe gesture: a finger, mouse or pen dragged it.

Controller

Create one with createDatePickerController() and pass it as controller.

getValue()() => DatePickerValue
The selected value: an ISO date, a [start, end] range, or null.
setValue(value, options?)(value: DatePickerValue, options?: DatePickerChangeOptions<DatePickerValueChangeReason> | undefined) => boolean
Selects a value. Returns false when canceled, unchanged, disabled, read-only, or holding a date people can't pick, like one past max.
clear(options?)(options?: DatePickerChangeOptions<DatePickerValueChangeReason> | undefined) => boolean
Selects nothing, as the Clear part does. Returns false when canceled or unchanged.
getMonth()() => string
The first month the calendar shows, like "2026-10".
setMonth(month, options?)(month: string, options?: DatePickerChangeOptions<DatePickerMonthChangeReason> | undefined) => boolean
Shows a month first. Returns false when canceled or unchanged.
previous(options?)(options?: DatePickerChangeOptions<DatePickerMonthChangeReason> | undefined) => boolean
Shows the months before, as the Previous part does.
next(options?)(options?: DatePickerChangeOptions<DatePickerMonthChangeReason> | undefined) => boolean
Shows the months after, as the Next part does.
focus(date?)(date?: string | undefined) => void
Focuses a date in the open calendar, turning the months to show it, or the date with the tab stop.
isOpen()() => boolean
Whether the popup is open.
setOpen(open, options?)(open: boolean, options?: DatePickerChangeOptions<DatePickerOpenChangeReason> | undefined) => boolean
Opens or closes the popup. Returns false when the change was canceled or it was already open or closed.
open(options?)(options?: (DatePickerChangeOptions<DatePickerOpenChangeReason> & { signal?: AbortSignal | undefined; }) | undefined) => Promise<...>
Opens the popup, and resolves once it closes, with why and how. Resolves exactly once and never rejects: on the next close from any source, when signal aborts, or with reason "unmount" when the date picker leaves the page.
close(options?)(options?: DatePickerChangeOptions<DatePickerOpenChangeReason> | undefined) => boolean
Closes the popup. Returns false when the close was canceled or it was already closed.
toggle(options?)(options?: DatePickerChangeOptions<DatePickerOpenChangeReason> | undefined) => boolean
Opens the popup if it's closed, and closes it if it's open. Returns false when the change was canceled.

Events

Events get the new value first, then the details. v-model only updates when the event wasn't canceled.

BindingOnReceives
v-model:valueDatePickerThe selected value
v-model:openDatePickerWhether the popup is open
v-model:monthDatePickerThe calendar's first month
@value-changeDatePicker(value, details): the new value, and the reason
@open-changeDatePicker(open, details): whether the popup opens, and the reason
@open-change-completeDatePicker(open): once the popup has finished opening or closing
@month-changeDatePicker(month, details): the calendar's new first month, and the reason
@dismiss-attemptDatePicker({ reason, event }), when a dismissal was refused

Accessibility

It follows the WAI-ARIA date picker dialog: a plain text input that every screen reader handles, a button with aria-haspopup="dialog" and aria-expanded named by the date chosen, and a dialog holding the calendar's grid. Focus moves into the grid when the dialog opens and back to the button when a date is picked or Escape closes it. Every line below is an automated test.

Keyboard

KeyWhereAction
EnterInputCommit the typed date.
Alt+Down ArrowInputOpen the calendar.
Enter / SpaceTriggerOpen or close the calendar.
EscapePopupCancel an unfinished range first; otherwise close the popup.
Tab / Shift+TabPopupMove through controls. Leaving the popup closes it when dismissal is allowed.

Opening focuses the selected date, otherwise today, otherwise the first focusable date. The calendarโ€™s arrow keys and month navigation work here too. An ancestorโ€™s dir="rtl" reverses Left and Right Arrow to follow the visual order.

Tested Behavior

  • A typed date in the page's format becomes the value on Enter, and the form submits it as ISO.
  • Text that isn't a date stays as typed and marks the input invalid.
  • Text that isn't a date blocks the form's submit until it's fixed.
  • Clearing text that isn't a date lets the form submit.
  • A message the app set on the input comes back once the text is a date.
  • The date picker element reports text that isn't a date as its own bad input. Web components only.
  • The format hint describes the input.
  • The trigger opens a dialog with focus on the selected date, and Escape closes it back to the trigger.
  • With nothing selected, focus goes to today.
  • Picking a day fills the input, closes the dialog and returns focus to the trigger.
  • With closeOnSelect off, the dialog stays open after a pick.
  • Alt+ArrowDown in the input opens the calendar.
  • A typed date opens the calendar on its month.
  • A press outside closes the dialog.
  • A preset selects its value and closes the dialog.
  • Clear empties the value and focuses the input.
  • A form reset puts the default value back.
  • A required date picker blocks an empty submit.
  • Days past min and max can't be typed or picked.
  • German reads and writes dates day first.
Range
  • Picking a start and an end fills both inputs and closes after the end.
  • Each end can be typed, and the value changes once both are set.
  • An end before the start is invalid.
  • An end that isn't a date blocks the form's submit until it's fixed.

Runs in React, Vue and web components.