DatePicker

A date field opening a calendar.

Usage

import { DatePicker } from "@voila.dev/ui/date-picker";

A field that looks like the rest of your form and opens a Calendar when clicked. This is the default choice for a date in a form: it costs one line of vertical space instead of a month. Reach past it when the date needs a time beside it — that is DateTimePicker — or when the grid should always be visible, which is Calendar on its own.

The value is a Date, and null means empty. calendarProps passes through to the grid underneath, so defaultMonth, disabled and the rest are set there rather than duplicated onto this component.

Recipes

Two dates, one field

DatePicker.Range selects { from, to }. Its onValueChange fires while the range is still half-drawn — once with from set and to undefined, then again when the second date lands. Guard on to before you turn it into a query, or the first click will fetch an open-ended range.

A different locale

locale reaches both the trigger label and the calendar, so the month names and the formatted date agree. For the label's format specifically, formatOptions takes Intl.DateTimeFormat options.

Set name and the picker writes a hidden input serialized as yyyy-MM-dd, which is what makes it work in a plain form post with no JavaScript on your side.

API

DatePicker.Root

PropTypeDefault
aria-invalid"false" | "grammar" | "spelling" | "true" | booleanMarks the trigger invalid. Pair it with your own message — this draws no text.
aria-labelstringAccessible name, for when there is no visible label to point id at.
calendarPropsCalendarPassthroughEscape hatch for the underlying Calendar (disabled days, week numbers…).
classNamestringClasses for the trigger. The popover is styled through calendarProps.
defaultOpenbooleanWhether the popover starts open. Uncontrolled — for the controlled form use open.
defaultValueDateInitial selection when uncontrolled.
disabledbooleanfalseBlocks the trigger, so the popover cannot be opened.
formatOptionsDateTimeFormatOptionsDEFAULT_DATE_FORMATIntl.DateTimeFormat options for the trigger label. Defaults to a long localized date ({ dateStyle: "long" }, e.g. "June 12, 2026" / "12 juin 2026").
idstringTies the trigger to a <label>. Pass it when the field has a visible label.
localestringBCP-47 locale (e.g. "fr-FR"), applied to both the trigger label and the calendar.
namestringName for the hidden form input(s); value(s) serialized as yyyy-MM-dd.
onOpenChange((open: boolean) => void)Called when the popover opens or closes, including on selection and on dismiss.
onValueChange((date: Date | null) => void)Called with the picked day, or null when the selection is cleared.
openbooleanControlled popover state. Pair it with onOpenChange.
placeholderstring"Pick a date"Shown on the trigger while nothing is selected. Name the field, don't say "Pick a date".
valueDate | nullControlled value; pass null for a controlled empty selection.
variant"brand" | "default" | "destructive" | "ghost" | "highlight" | "link" | "outline" | "primary" | "secondary"Button variant for the trigger. outline is the field-shaped default.

Plus the DOM props of the element it renders. Source: date-picker/components/date-picker-root.tsx.

DatePicker.Range

PropTypeDefault
aria-invalid"false" | "grammar" | "spelling" | "true" | booleanMarks the trigger invalid. Pair it with your own message — this draws no text.
aria-labelstringAccessible name, for when there is no visible label to point id at.
calendarPropsCalendarPassthroughEscape hatch for the underlying Calendar (disabled days, week numbers…).
classNamestringClasses for the trigger. The popover is styled through calendarProps.
defaultOpenbooleanWhether the popover starts open. Uncontrolled — for the controlled form use open.
defaultValueDateRangeInitial selection when uncontrolled.
disabledbooleanfalseBlocks the trigger, so the popover cannot be opened.
formatOptionsDateTimeFormatOptionsDEFAULT_DATE_FORMATIntl.DateTimeFormat options for the trigger label. Defaults to a long localized date ({ dateStyle: "long" }, e.g. "June 12, 2026" / "12 juin 2026").
idstringTies the trigger to a <label>. Pass it when the field has a visible label.
localestringBCP-47 locale (e.g. "fr-FR"), applied to both the trigger label and the calendar.
namestringName for the hidden form input(s); value(s) serialized as yyyy-MM-dd.
onOpenChange((open: boolean) => void)Called when the popover opens or closes, including on selection and on dismiss.
onValueChange((range: DateRange | null) => void)Called as the range is built, so it fires once with only from set before firing again with both. Guard on to before querying.
openbooleanControlled popover state. Pair it with onOpenChange.
placeholderstring"Pick a date range"Shown on the trigger while nothing is selected. Name the field, don't say "Pick a date".
valueDateRange | nullControlled value; pass null for a controlled empty selection.
variant"brand" | "default" | "destructive" | "ghost" | "highlight" | "link" | "outline" | "primary" | "secondary"Button variant for the trigger. outline is the field-shaped default.

Plus the DOM props of the element it renders. Source: date-picker/components/date-range-picker.tsx.

DatePicker.Trigger

PropTypeDefault
emptybooleanrequiredWhether nothing is selected, which is what renders the muted placeholder.
iconReactNode( <CalendarBlankIcon className="size-4 shrink-0 text-muted-foreground" /> )Leading icon. Defaults to a calendar glyph.
loadingbooleanShow a leading spinner and mark the button aria-busy, disabling interaction while an async action is in flight (e.g. a form submit).
shape"default" | "pill"pill forces a fully rounded button; default follows the theme's --radius, so it changes with the brand.
size"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs"Height and horizontal padding. The icon-* set is square, for a button whose entire content is one icon — give those an aria-label.
slotNamestring"date-picker-trigger"Base for the part's data-slot attribute; not the DOM slot.
variant"brand" | "default" | "destructive" | "ghost" | "highlight" | "link" | "outline" | "primary" | "secondary""outline"Weight of the action. default is the one thing the page exists for, at most one per view; secondary an equal-weight alternative; outline for toolbars and anything that repeats in a row; ghost inside dense surfaces; destructive for what cannot be undone; link for an action that reads as navigation.
classNamestring | ((state: ButtonState) => string | undefined)CSS class applied to the element, or a function that returns a class based on the component's state.
focusableWhenDisabledbooleanWhether the button should be focusable when disabled.
nativeButtonbooleanWhether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
renderComponentRenderFn<HTMLProps, ButtonState> | ReactElement<unknown, string | JSXElementConstructor<any>>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleCSSProperties | ((state: ButtonState) => CSSProperties | undefined)Style applied to the element, or a function that returns a style object based on the component's state.

Plus the DOM props of the element it renders. Source: date-picker/components/date-picker-trigger.tsx.

The field on its own, without the popover. Use it when you are building a picker whose surface is not a calendar and you want the trigger to match the others.

Calendar · DateTimePicker · TimePicker · Field