Skip to content
Component installs need the registry setup
VegaStack Design

Date Picker

Pick a single date or a date range from a calendar popover — token-styled, keyboard-navigable, with optional quick presets.

Status
stable
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-picker

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/date-picker

Run 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):

DatePicker — data-slot="date-picker" | "date-picker-clear" | "date-picker-content" | "date-picker-trigger"
DateRangePicker — data-slot="date-range-picker-content" | "date-range-picker-trigger"

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 outline Button (data-slot="date-picker-trigger") shows the formatted date and opens the calendar in a Popover (data-slot="date-picker-content"). Pass calendarProps for the DayPicker knobs Calendar forwards — 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 accepts calendarProps; top-level selection props remain owned by the wrapper.
  • defaultDatePresets() / defaultRangePresets() — ready-made quick-select rails (Today / Tomorrow, Last 7 / 30 days). Pass your own presets to override, and { timeZone: useTimeZone() } so "Today" is the viewer's day.
  • Today in the grid follows the TimeZoneProvider zone: both pickers mark the provider zone's calendar day as today (a calendarProps.today or calendarProps.timeZone wins). A bare Calendar is upstream's and takes today or timeZone directly.

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

Renew the SSL certificate

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.

Default — Jun 24, 2026
With weekday
Long month
Numeric
Locale — de-DE

API Reference

DatePicker

PropTypeDefaultDescription
alignAlign"start"Popover alignment relative to the trigger.
aria-describedbystring—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-labelstring—Accessible name for the trigger (recommended when there is no visible label).
calendarPropsForwardedCalendarProps—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.
classNamestring—Extra classes for the trigger button.
clearablebooleanfalseShows 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.
clearLabelstring"Clear date"Accessible name for the clear control.
disabledboolean—Disable the whole control.
disabledDatesMatcher[] | Matcher—Dates to disable, forwarded to the calendar's disabled matcher.
formatOptionsIntl.DateTimeFormatOptions{ year: 'numeric', month: 'short', day: 'numeric' }Intl.DateTimeFormat options for the trigger label.
idstring—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.
localestring—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.
placeholderstring"Pick a date"Trigger text shown when no date is selected.
presetsDatePreset[]—Quick-select presets shown in a left rail. Omit for no presets.
refReact.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)}\``.
sideSide"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.
valueDate—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

AttributeValues
data-empty""
data-sizemirrors a prop or state value
data-slot"date-picker" | "date-picker-clear" | "date-picker-content" | "date-picker-trigger"
data-variantmirrors a prop or state value

DateRangePicker

PropTypeDefaultDescription
alignAlign"start"Popover alignment relative to the trigger.
aria-describedbystring—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-labelstring—Accessible name for the trigger (recommended when there is no visible label).
calendarPropsForwardedCalendarProps—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.
classNamestring—Extra classes for the trigger button.
disabledboolean—Disable the whole control.
disabledDatesMatcher[] | Matcher—Dates to disable, forwarded to the calendar's disabled matcher.
formatOptionsIntl.DateTimeFormatOptions{ year: 'numeric', month: 'short', day: 'numeric' }Intl.DateTimeFormat options for each end of the trigger label.
idstring—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.
localestring—BCP-47 locale for formatting (defaults to DEFAULT_LOCALE, en-US, so the server and the browser agree).
numberOfMonthsnumber2Number of month grids to show side by side.
onValueChange((range: DateRange | undefined) => void)—Fires with the new range (or undefined when cleared) on selection.
placeholderstring"Pick a date range"Trigger text shown when no range is selected.
presetsDateRangePreset[]—Quick-select presets shown in a left rail. Omit for no presets.
sideSide"bottom"Popover side relative to the trigger.
valueDateRange—The selected range (controlled).

Data attributes and CSS variables on DateRangePicker

AttributeValues
data-empty""
data-slot"date-range-picker-content" | "date-range-picker-trigger"

DatePreset

The shape of each entry in DatePicker's presets array.

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

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

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

PropTypeDefaultDescription
DateRange{ from: Date | undefined; to?: Date | undefined }—A selected range. Used by DateRangePicker's value / onValueChange and DateRangePreset.range.
MatcherDate | 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 a grid of gridcell day buttons with descriptive aria-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-primary for a selection, bg-muted for 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-visible outline, and the day grid manages roving focus itself.
  • The trigger has no visible text label when empty beyond its placeholder, so name it: pass id and a FieldLabel htmlFor pointing at it (clicking the label then opens the picker), or aria-label when there is no visible label. aria-describedby and aria-invalid are forwarded to the trigger on both pickers.
KeyAction
Enter / SpaceOpen the picker (on the trigger) or select the focused day (in the grid).
↑ ↓ ← →Move focus between days (±1 week / ±1 day).
PageUp / PageDownMove to the previous / next month.
Home / EndMove to the first / last day of the week.
EscClose the popover and return focus to the trigger.
ContractStates tested
Behaviourdefault, complete, disabled, empty, open, range, selected
Accessibilitydisabled, labeled, selected, semantic-html
Visualdefault, hover, disabled, selected, empty

Do / Don't

Do
Use a DatePicker for a single calendar date and a DateRangePicker for a span. Give the trigger an aria-label (or a Field label), and pass disabledDates to block invalid days. Format with the consumer's locale via the locale prop.
Don't
Don't use a date picker for free-form or partial dates (a plain Input with a mask is better), and don't rely on color alone to mark the selection — the state styling already encodes selection, today, and disabled.

On this page