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.

Three surfaces

Every picker family here has the same three: Root is the composed popover above, Native is the OS control, and Responsive renders Root on a pointer and Native under the useIsMobile breakpoint (768px). One Date | null value model across all three, so switching surface is a one-word edit.

Responsive is the default choice for a form. Reach for Root only when the surface must look identical everywhere, and for Native only when you want the OS control on desktop too.

The OS control

One field, both surfaces

min and max bound both surfaces at once: the native field's attributes, and on desktop the range of the year dropdown plus the disabled days around it. Responsive always draws month and year dropdowns — a birth date is unreachable by paging a month at a time. Without bounds the dropdown still spans a century back and ten years forward, because react-day-picker's own fallback stops at the end of the current year and hides every future date.

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 range on any viewport

ResponsiveRange and NativeRange are two bound fields rather than one two-month popover: no browser ships a native range input, and two months of day cells do not fit a phone. Each side bounds the other, so an inverted range cannot be entered, and a new start that overtakes the end clears it instead of leaving something impossible on screen.

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.Native

PropTypeDefault
classNamestringClasses for the field's box, so className means the same thing here as on the popover surfaces. The inner <input> keeps the kit's field styling; reach it through inputClassName on the rare occasion you must.
inputClassNamestringClasses for the inner <input> itself.
maxDateLatest selectable day.
minDateEarliest selectable day.
onValueChange((date: Date | null) => void)Called with the picked day, or null when the field is emptied.
size"default" | "sm"Field height. sm for dense surfaces such as table cells and toolbars.
valueDate | nullControlled value; pass null for a controlled empty selection.

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

DatePicker.Responsive

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 field itself: the popover trigger on desktop, the wrapper around the native input on mobile. Use it for layout and width (w-full), which is what both surfaces share.
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.
formatOptionsDateTimeFormatOptionsIntl.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.
maxDateLatest selectable day. Bounds the year dropdown and the native field.
minDateEarliest selectable day. Bounds the year dropdown and the native field.
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.
placeholderstringShown 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/responsive-date-input.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.NativeRange

PropTypeDefault
aria-invalid"false" | "grammar" | "spelling" | "true" | booleanMarks both fields invalid. Pair it with your own message.
classNamestringStyles the wrapping grid (e.g. md:col-span-2).
defaultValueDateRangeInitial range when uncontrolled.
disabledbooleanBlocks both fields.
fromIdstringid for the start field; the end field derives ${fromId}-to when toId is omitted.
fromLabelReactNodeLabel above the start field. Defaults to "From".
fromPlaceholderstringPlaceholder for the start field while it is empty.
localestringBCP-47 locale (e.g. "fr-FR"), applied to both fields.
maxDateLatest day either side may take.
minDateEarliest day either side may take.
namestringNames for the hidden form inputs: ${name}-from and ${name}-to.
onValueChange((range: DateRange) => void)Called with the whole { from, to } after either side changes.
toIdstringid for the end field. Derived from fromId when omitted.
toLabelReactNodeLabel above the end field. Defaults to "To".
toPlaceholderstringPlaceholder for the end field while it is empty.
valueDateRange | nullControlled range; pass null for a controlled empty selection.

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

DatePicker.ResponsiveRange

PropTypeDefault
aria-invalid"false" | "grammar" | "spelling" | "true" | booleanMarks both fields invalid. Pair it with your own message.
classNamestringStyles the wrapping grid (e.g. md:col-span-2).
defaultValueDateRangeInitial range when uncontrolled.
disabledbooleanBlocks both fields.
fromIdstringid for the start field; the end field derives ${fromId}-to when toId is omitted.
fromLabelReactNodeLabel above the start field. Defaults to "From".
fromPlaceholderstringPlaceholder for the start field while it is empty.
localestringBCP-47 locale (e.g. "fr-FR"), applied to both fields.
maxDateLatest day either side may take.
minDateEarliest day either side may take.
namestringNames for the hidden form inputs: ${name}-from and ${name}-to.
onValueChange((range: DateRange) => void)Called with the whole { from, to } after either side changes.
toIdstringid for the end field. Derived from fromId when omitted.
toLabelReactNodeLabel above the end field. Defaults to "To".
toPlaceholderstringPlaceholder for the end field while it is empty.
valueDateRange | nullControlled range; pass null for a controlled empty selection.

Plus the DOM props of the element it renders. Source: date-picker/components/responsive-date-range-input.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 · NativeDatePicker · Field