Date Picker
Pick a single date or a date range from a calendar popover — token-styled, keyboard-navigable, with optional quick presets.
- Status
- Since
0.1.0- Accessibility pattern
- APG date picker dialog
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Date Picker before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify date-pickerInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/date-pickerRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/date-time, @vegastack/calendar, @vegastack/popover, @vegastack/button, @vegastack/relative-time.
It also adds the sanctioned engine to your package.json: react-day-picker (calendar interaction/rendering engine).
Usage
import { DatePicker } from "@/components/ui/date-picker";
function Example() {
const [date, setDate] = React.useState<Date>();
return <DatePicker value={date} onValueChange={setDate} />;
}Inside a Field, give the trigger an id and point the FieldLabel at it, exactly as for an
Input. aria-describedby and aria-invalid reach the trigger too, so a description, an error and
the invalid border work the same way:
<Field data-invalid={!!error}>
<FieldLabel htmlFor="due-date">Due date</FieldLabel>
<DatePicker
id="due-date"
value={date}
onValueChange={setDate}
aria-describedby="due-date-error"
aria-invalid={!!error}
/>
<FieldError id="due-date-error">{error}</FieldError>
</Field>The calendar engine (react-day-picker and date-fns) is not in the page's first bundle: both
pickers fetch it when the trigger is hovered or focused and render it once the popup opens, so a
form full of date fields costs nothing until one is used. Nothing to configure — the popup looks and
behaves the same.
Anatomy
Date Picker is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Anatomy
The Date Picker module is a composition, not a calendar: the grid is
Calendar, which this module imports rather than restates. Both
pickers put that calendar inside a Popover opened by an outline
Button. For a grid with no popover, use Calendar directly.
// Single date — Button-triggered Popover hosting the Calendar.
<DatePicker
value={date}
onValueChange={setDate}
presets={defaultDatePresets()}
calendarProps={{ timeZone: 'UTC', captionLayout: 'dropdown' }}
/>
// Range — two-month grid, closes once both ends are chosen.
<DateRangePicker value={range} onValueChange={setRange} />DatePicker— single date. An outlineButton(data-slot="date-picker-trigger") shows the formatted date and opens the calendar in aPopover(data-slot="date-picker-content"). PasscalendarPropsfor theDayPickerknobsCalendarforwards —timeZone,locale,footer,captionLayout,startMonth,endMonth,labels,formatters.DateRangePicker— a{ from, to }range, same shape, across two months (data-slot="date-range-picker-trigger"/…-content). It also acceptscalendarProps; top-level selection props remain owned by the wrapper.defaultDatePresets()/defaultRangePresets()— ready-made quick-select rails (Today / Tomorrow, Last 7 / 30 days). Pass your ownpresetsto override, and{ timeZone: useTimeZone() }so "Today" is the viewer's day.- Today in the grid follows the
TimeZoneProviderzone: both pickers mark the provider zone's calendar day as today (acalendarProps.todayorcalendarProps.timeZonewins). A bareCalendaris upstream's and takestodayortimeZonedirectly.
With presets
A left rail of quick-select buttons (Today / Tomorrow) sits beside the calendar. Selecting a preset closes the popover.
Date range
Pick a start and end across two months. The popover stays open until both ends are chosen; presets cover common windows.
Disabled dates
Pass disabledDates (a single Matcher, or an array) to block days in the grid and gate presets. A blocked day renders at reduced opacity and can't be selected; a preset whose date — or, for ranges, any day it spans — falls inside the blocked set is rendered inert and never emits, using the same react-day-picker matcher evaluator the grid applies, so the preset gate cannot drift from the grid.
Disabled
Set disabled to make the whole control inert — the trigger reads dimmed and won't open the popover.
Inside a Field
Inside a Field the trigger renders through Base UI's Field.Control,
so FieldLabel names it, the rendered FieldDescription and FieldError describe it, and the
Field's data-invalid marks it invalid. An explicit id still wins, and an explicit
aria-describedby keeps its ids first.
When the task is due.
Clearable
clearable puts a clear control beside the trigger while a date is set — a sibling, never inside
the trigger — named by clearLabel ("Clear date"). Clearing reports undefined and returns focus
to the trigger.
Inline in a row
For a date inside a table row or a toolbar, size="sm" matches Select size="sm" and
SearchableSelect size="sm", and variant="ghost" sizes the trigger to its content with no border
at rest. renderValue replaces the formatted date with your own face, such as "Due Jun 18".
Dropdown caption
Forward Calendar's own DayPicker knobs through calendarProps. Here captionLayout="dropdown" (bounded by startMonth / endMonth) swaps the static month/year heading for navigable dropdowns — handy for jumping across years.
<DatePicker
value={date}
onValueChange={setDate}
calendarProps={{
captionLayout: "dropdown",
startMonth: new Date(2024, 0),
endMonth: new Date(2027, 11),
}}
/>Single-month range
DateRangePicker shows two months by default; pass numberOfMonths={1} for a compact single-month grid that fits tight layouts.
Custom formatting & locale
The trigger label is formatted with native Intl.DateTimeFormat — no date library. The
default is the readable Jun 24, 2026 ({ year: 'numeric', month: 'short', day: 'numeric' }),
never an ISO string and never the browser's date-input format. Pass formatOptions for any
other shape and locale for a BCP-47 locale.
Prerendering a formatted date needs an explicit locale. With no
locale, the label follows whichever locale the runtime resolves — which is
the build machine on the server and the visitor's browser on the client. When
those differ (Jun 24, 2026 against 24 Jun 2026) React reports a hydration
mismatch and re-renders the tree. Pass locale on any picker that renders a
value in server-rendered or statically exported markup; every example on this
page does.
Use DatePicker wherever you'd reach for <input type="date">. The native input summons the
OS picker, which ignores the design tokens and differs per browser and platform; this
component is themed, keyboard-accessible, and formats consistently everywhere.
API Reference
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | "start" | Popover alignment relative to the trigger. |
aria-describedby | string | — | Ids of the elements describing the trigger — a FieldDescription or FieldError. |
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks the trigger invalid; the outline Button shows its destructive border, exactly as an
invalid Input or SelectTrigger does inside a Field data-invalid. |
aria-label | string | — | Accessible name for the trigger (recommended when there is no visible label). |
calendarProps | ForwardedCalendarProps | — | Props forwarded to the inner Calendar for DayPicker features such as
timeZone, locale, footer, captionLayout, startMonth,
endMonth, labels, and formatters. Selection ownership stays with
DatePicker, so mode, selected, onSelect, and disabled are not
accepted here. |
className | string | — | Extra classes for the trigger button. |
clearable | boolean | false | Shows a clear control beside the trigger while a date is set, which reports undefined.
It is a sibling of the trigger, never inside it, and focus returns to the trigger after
clearing. |
clearLabel | string | "Clear date" | Accessible name for the clear control. |
disabled | boolean | — | Disable the whole control. |
disabledDates | Matcher[] | Matcher | — | Dates to disable, forwarded to the calendar's disabled matcher. |
formatOptions | Intl.DateTimeFormatOptions | { year: 'numeric', month: 'short', day: 'numeric' } | Intl.DateTimeFormat options for the trigger label. |
id | string | — | id for the trigger button, so a FieldLabel htmlFor (or any <label for>) names it and
clicking the label opens the picker — the same binding every other form control takes. |
locale | string | — | BCP-47 locale for formatting (defaults to DEFAULT_LOCALE, en-US, so the server and the browser agree). |
onValueChange | ((date: Date | undefined) => void) | — | Fires with the new date (or undefined when cleared) on selection. |
placeholder | string | "Pick a date" | Trigger text shown when no date is selected. |
presets | DatePreset[] | — | Quick-select presets shown in a left rail. Omit for no presets. |
ref | React.Ref<HTMLButtonElement> | — | Ref forwarded to the trigger button. |
renderValue | ((date: Date) => React.ReactNode) | — | Renders the selected date on the trigger in place of the formatted date — e.g.
(date) => \Due ${format(date)}\``. |
side | Side | "bottom" | Popover side relative to the trigger. |
size | "default" | "sm" | 'default' | Trigger height: sm (28px) is the inline tier shared with Select size="sm" and
SearchableSelect size="sm", for table rows and toolbars. |
value | Date | — | The selected date (controlled). |
variant | "ghost" | "outline" | 'outline' | ghost is the inline trigger: content width, no border at rest, the border on hover, on
focus and while the calendar is open — the same tier as Select variant="ghost". |
Data attributes and CSS variables on DatePicker
| Attribute | Values |
|---|---|
data-empty | "" |
data-size | mirrors a prop or state value |
data-slot | "date-picker" | "date-picker-clear" | "date-picker-content" | "date-picker-trigger" |
data-variant | mirrors a prop or state value |
DateRangePicker
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | "start" | Popover alignment relative to the trigger. |
aria-describedby | string | — | Ids of the elements describing the trigger — a FieldDescription or FieldError. |
aria-invalid | "false" | "grammar" | "spelling" | "true" | boolean | — | Marks the trigger invalid; the outline Button shows its destructive border, exactly as an
invalid Input or SelectTrigger does inside a Field data-invalid. |
aria-label | string | — | Accessible name for the trigger (recommended when there is no visible label). |
calendarProps | ForwardedCalendarProps | — | Props forwarded to the inner Calendar for DayPicker features such as
timeZone, locale, footer, captionLayout, startMonth,
endMonth, labels, and formatters. Selection ownership stays with
DateRangePicker, so mode, selected, onSelect, and disabled are
not accepted here. |
className | string | — | Extra classes for the trigger button. |
disabled | boolean | — | Disable the whole control. |
disabledDates | Matcher[] | Matcher | — | Dates to disable, forwarded to the calendar's disabled matcher. |
formatOptions | Intl.DateTimeFormatOptions | { year: 'numeric', month: 'short', day: 'numeric' } | Intl.DateTimeFormat options for each end of the trigger label. |
id | string | — | id for the trigger button, so a FieldLabel htmlFor (or any <label for>) names it and
clicking the label opens the picker — the same binding every other form control takes. |
locale | string | — | BCP-47 locale for formatting (defaults to DEFAULT_LOCALE, en-US, so the server and the browser agree). |
numberOfMonths | number | 2 | Number of month grids to show side by side. |
onValueChange | ((range: DateRange | undefined) => void) | — | Fires with the new range (or undefined when cleared) on selection. |
placeholder | string | "Pick a date range" | Trigger text shown when no range is selected. |
presets | DateRangePreset[] | — | Quick-select presets shown in a left rail. Omit for no presets. |
side | Side | "bottom" | Popover side relative to the trigger. |
value | DateRange | — | The selected range (controlled). |
Data attributes and CSS variables on DateRangePicker
| Attribute | Values |
|---|---|
data-empty | "" |
data-slot | "date-range-picker-content" | "date-range-picker-trigger" |
DatePreset
The shape of each entry in DatePicker's presets array.
| Prop | Type | Default | Description |
|---|---|---|---|
date* | Date | — | The date this preset selects. |
label* | string | — | Button text, e.g. "Today". |
DateRangePreset
The shape of each entry in DateRangePicker's presets array.
| Prop | Type | Default | Description |
|---|---|---|---|
label* | string | — | Button text, e.g. "Last 7 days". |
range* | DateRange | — | The { from, to } range this preset selects. |
Preset helpers
Ready-made preset rails. Pass your own presets to override either.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultDatePresets | (now?: Date, options?: { timeZone?: string }) => DatePreset[] | — | Today / Tomorrow presets for a single DatePicker. Accepts an optional reference date (defaults to now) and the zone whose calendar day is today — pass useTimeZone(). |
defaultRangePresets | (now?: Date, options?: { timeZone?: string }) => DateRangePreset[] | — | Today / Last 7 days / Last 30 days presets for a DateRangePicker. Accepts an optional reference date (defaults to now) and the zone whose calendar day is today. |
DateRange & Matcher
Re-exported from react-day-picker. DateRange is the shape of a range value; Matcher is what disabledDates accepts (a single matcher or an array).
| Prop | Type | Default | Description |
|---|---|---|---|
DateRange | { from: Date | undefined; to?: Date | undefined } | — | A selected range. Used by DateRangePicker's value / onValueChange and DateRangePreset.range. |
Matcher | Date | Date[] | DateRange | DayOfWeek | DateBefore | DateAfter | DateInterval | ((date: Date) => boolean) | — | A react-day-picker day predicate. Passed (singly or as an array) to disabledDates to block days. |
Accessibility
- The grid is
Calendar, which renders the month as agridofgridcellday buttons with descriptivearia-labels ("Saturday, June 21st, 2026"); its own page documents the grid's semantics in full. - The selected day, range ends, today, and disabled days are all distinguishable by state — not by colour alone — via
data-*driven surfaces (bg-primaryfor a selection,bg-mutedfor today, reduced opacity for a blocked day). - Opening the picker moves focus into the calendar; arrow keys move day-to-day, and selection restores focus to the trigger. Pressing Esc closes the popover.
- Opening a picker that sits below the fold does not scroll the page: the popover's day button focuses with
preventScroll, because the portaled popup is still unpositioned on its first paint. - Focus is always visible — the trigger takes the global
:focus-visibleoutline, and the day grid manages roving focus itself. - The trigger has no visible text label when empty beyond its placeholder, so name it: pass
idand aFieldLabel htmlForpointing at it (clicking the label then opens the picker), oraria-labelwhen there is no visible label.aria-describedbyandaria-invalidare forwarded to the trigger on both pickers.
| Key | Action |
|---|---|
| Enter / Space | Open the picker (on the trigger) or select the focused day (in the grid). |
| ↑ ↓ ← → | Move focus between days (±1 week / ±1 day). |
| PageUp / PageDown | Move to the previous / next month. |
| Home / End | Move to the first / last day of the week. |
| Esc | Close the popover and return focus to the trigger. |
| Contract | States tested |
|---|---|
| Behaviour | default, complete, disabled, empty, open, range, selected |
| Accessibility | disabled, labeled, selected, semantic-html |
| Visual | default, hover, disabled, selected, empty |
Do / Don't
Region Select
A searchable combobox of states/provinces for a country — Combobox-powered filtering, with a free-text fallback for countries with no subdivisions.
Calendar
A date-field calendar built on React DayPicker — single, multiple and range selection, month and year dropdowns, week numbers and custom cell sizes.