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
| Prop | Type | Default | |
|---|---|---|---|
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks the trigger invalid. Pair it with your own message — this draws no text. |
aria-label | string | — | Accessible name, for when there is no visible label to point id at. |
calendarProps | CalendarPassthrough | — | Escape hatch for the underlying Calendar (disabled days, week numbers…). |
className | string | — | Classes for the trigger. The popover is styled through calendarProps. |
defaultOpen | boolean | — | Whether the popover starts open. Uncontrolled — for the controlled form use open. |
defaultValue | Date | — | Initial selection when uncontrolled. |
disabled | boolean | false | Blocks the trigger, so the popover cannot be opened. |
formatOptions | DateTimeFormatOptions | DEFAULT_DATE_FORMAT | Intl.DateTimeFormat options for the trigger label. Defaults to a long
localized date ({ dateStyle: "long" }, e.g. "June 12, 2026" / "12 juin 2026"). |
id | string | — | Ties the trigger to a <label>. Pass it when the field has a visible label. |
locale | string | — | BCP-47 locale (e.g. "fr-FR"), applied to both the trigger label and the calendar. |
name | string | — | Name 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. |
open | boolean | — | Controlled popover state. Pair it with onOpenChange. |
placeholder | string | "Pick a date" | Shown on the trigger while nothing is selected. Name the field, don't say "Pick a date". |
value | Date | null | — | Controlled 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
| Prop | Type | Default | |
|---|---|---|---|
className | string | — | Classes 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. |
inputClassName | string | — | Classes for the inner <input> itself. |
max | Date | — | Latest selectable day. |
min | Date | — | Earliest 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. |
value | Date | null | — | Controlled 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
| Prop | Type | Default | |
|---|---|---|---|
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks the trigger invalid. Pair it with your own message — this draws no text. |
aria-label | string | — | Accessible name, for when there is no visible label to point id at. |
calendarProps | CalendarPassthrough | — | Escape hatch for the underlying Calendar (disabled days, week numbers…). |
className | string | — | Classes 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. |
defaultOpen | boolean | — | Whether the popover starts open. Uncontrolled — for the controlled form use open. |
defaultValue | Date | — | Initial selection when uncontrolled. |
disabled | boolean | false | Blocks the trigger, so the popover cannot be opened. |
formatOptions | DateTimeFormatOptions | — | Intl.DateTimeFormat options for the trigger label. Defaults to a long
localized date ({ dateStyle: "long" }, e.g. "June 12, 2026" / "12 juin 2026"). |
id | string | — | Ties the trigger to a <label>. Pass it when the field has a visible label. |
locale | string | — | BCP-47 locale (e.g. "fr-FR"), applied to both the trigger label and the calendar. |
max | Date | — | Latest selectable day. Bounds the year dropdown and the native field. |
min | Date | — | Earliest selectable day. Bounds the year dropdown and the native field. |
name | string | — | Name 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. |
open | boolean | — | Controlled popover state. Pair it with onOpenChange. |
placeholder | string | — | Shown on the trigger while nothing is selected. Name the field, don't say "Pick a date". |
value | Date | null | — | Controlled 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
| Prop | Type | Default | |
|---|---|---|---|
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks the trigger invalid. Pair it with your own message — this draws no text. |
aria-label | string | — | Accessible name, for when there is no visible label to point id at. |
calendarProps | CalendarPassthrough | — | Escape hatch for the underlying Calendar (disabled days, week numbers…). |
className | string | — | Classes for the trigger. The popover is styled through calendarProps. |
defaultOpen | boolean | — | Whether the popover starts open. Uncontrolled — for the controlled form use open. |
defaultValue | DateRange | — | Initial selection when uncontrolled. |
disabled | boolean | false | Blocks the trigger, so the popover cannot be opened. |
formatOptions | DateTimeFormatOptions | DEFAULT_DATE_FORMAT | Intl.DateTimeFormat options for the trigger label. Defaults to a long
localized date ({ dateStyle: "long" }, e.g. "June 12, 2026" / "12 juin 2026"). |
id | string | — | Ties the trigger to a <label>. Pass it when the field has a visible label. |
locale | string | — | BCP-47 locale (e.g. "fr-FR"), applied to both the trigger label and the calendar. |
name | string | — | Name 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. |
open | boolean | — | Controlled popover state. Pair it with onOpenChange. |
placeholder | string | "Pick a date range" | Shown on the trigger while nothing is selected. Name the field, don't say "Pick a date". |
value | DateRange | null | — | Controlled 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
| Prop | Type | Default | |
|---|---|---|---|
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks both fields invalid. Pair it with your own message. |
className | string | — | Styles the wrapping grid (e.g. md:col-span-2). |
defaultValue | DateRange | — | Initial range when uncontrolled. |
disabled | boolean | — | Blocks both fields. |
fromId | string | — | id for the start field; the end field derives ${fromId}-to when toId is omitted. |
fromLabel | ReactNode | — | Label above the start field. Defaults to "From". |
fromPlaceholder | string | — | Placeholder for the start field while it is empty. |
locale | string | — | BCP-47 locale (e.g. "fr-FR"), applied to both fields. |
max | Date | — | Latest day either side may take. |
min | Date | — | Earliest day either side may take. |
name | string | — | Names for the hidden form inputs: ${name}-from and ${name}-to. |
onValueChange | ((range: DateRange) => void) | — | Called with the whole { from, to } after either side changes. |
toId | string | — | id for the end field. Derived from fromId when omitted. |
toLabel | ReactNode | — | Label above the end field. Defaults to "To". |
toPlaceholder | string | — | Placeholder for the end field while it is empty. |
value | DateRange | null | — | Controlled 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
| Prop | Type | Default | |
|---|---|---|---|
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks both fields invalid. Pair it with your own message. |
className | string | — | Styles the wrapping grid (e.g. md:col-span-2). |
defaultValue | DateRange | — | Initial range when uncontrolled. |
disabled | boolean | — | Blocks both fields. |
fromId | string | — | id for the start field; the end field derives ${fromId}-to when toId is omitted. |
fromLabel | ReactNode | — | Label above the start field. Defaults to "From". |
fromPlaceholder | string | — | Placeholder for the start field while it is empty. |
locale | string | — | BCP-47 locale (e.g. "fr-FR"), applied to both fields. |
max | Date | — | Latest day either side may take. |
min | Date | — | Earliest day either side may take. |
name | string | — | Names for the hidden form inputs: ${name}-from and ${name}-to. |
onValueChange | ((range: DateRange) => void) | — | Called with the whole { from, to } after either side changes. |
toId | string | — | id for the end field. Derived from fromId when omitted. |
toLabel | ReactNode | — | Label above the end field. Defaults to "To". |
toPlaceholder | string | — | Placeholder for the end field while it is empty. |
value | DateRange | null | — | Controlled 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
| Prop | Type | Default | |
|---|---|---|---|
empty | boolean | required | Whether nothing is selected, which is what renders the muted placeholder. |
icon | ReactNode | ( <CalendarBlankIcon className="size-4 shrink-0 text-muted-foreground" /> ) | Leading icon. Defaults to a calendar glyph. |
loading | boolean | — | Show 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. |
slotName | string | "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. |
className | string | ((state: ButtonState) => string | undefined) | — | CSS class applied to the element, or a function that returns a class based on the component's state. |
focusableWhenDisabled | boolean | — | Whether the button should be focusable when disabled. |
nativeButton | boolean | — | Whether 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>). |
render | ComponentRenderFn<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. |
style | CSSProperties | ((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.
Related
Calendar · DateTimePicker · TimePicker · NativeDatePicker · Field