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