Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
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

June 2026

Install

Add Date Picker from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

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

The same command installs the registry items it composes: @vegastack/popover, @vegastack/button.

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} />;
}

Anatomy

Date Picker is a compound component. Every exported part, with the data-slot it renders (generated from the canonical source):

Calendar — data-slot="calendar"
CalendarDayButton — data-slot="calendar-day"
DatePicker — data-slot="date-picker-content" | "date-picker-trigger"
DateRangePicker — data-slot="date-range-picker-content" | "date-range-picker-trigger"

Examples

Anatomy

The Date Picker module exports three building blocks. DatePicker and DateRangePicker compose a Calendar inside a Popover; use Calendar directly when you want an inline grid.

// Single date — Button-triggered Popover hosting a Calendar.
<DatePicker
  value={date}
  onValueChange={setDate}
  presets={defaultDatePresets()}
  calendarProps={{ timeZone: 'UTC', captionLayout: 'dropdown' }}
/>

// Range — two-month Calendar, closes once both ends are chosen.
<DateRangePicker value={range} onValueChange={setRange} />

// Inline — the styled DayPicker with no popover.
<Calendar mode="single" selected={date} onSelect={setDate} />
  • Calendar — a token-styled react-day-picker DayPicker (data-slot="calendar"). Forwards every DayPicker prop (mode, selected, onSelect, defaultMonth, numberOfMonths, disabled, …); overrides the nav chevrons (lucide) and the day button.
  • CalendarDayButton — the per-day Button (data-slot="calendar-day"). Paints selection/range/today state from data-* attributes and auto-focuses on keyboard navigation.
  • 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 DayPicker knobs such as timeZone, locale, footer, captionLayout, startMonth, endMonth, labels, and 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.

Date and range selection

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.

Inline calendar

The bare Calendar with no popover — drop it into a card or sidebar.

June 2026

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 the grid applies (so the preset gate can't drift from the grid).

June 2026

Disabled

Set disabled to make the whole control inert — the trigger reads dimmed and won't open the popover.

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

June 2026
<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-labelstringAccessible name for the trigger (recommended when there is no visible label).
calendarPropsOmit<CalendarProps, "disabled" | "mode" | "onSelect" | "selected">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.
classNamestringExtra classes for the trigger button.
disabledbooleanDisable the whole control.
disabledDatesMatcher[] | MatcherDates to disable, forwarded to the calendar's disabled matcher.
formatOptionsIntl.DateTimeFormatOptions{ year: 'numeric', month: 'short', day: 'numeric' }Intl.DateTimeFormat options for the trigger label.
localestringBCP-47 locale for formatting (defaults to the runtime locale).
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.
sideSide"bottom"Popover side relative to the trigger.
valueDateThe selected date (controlled).

Data attributes and CSS variables on DatePicker

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

DateRangePicker

PropTypeDefaultDescription
alignAlign"start"Popover alignment relative to the trigger.
aria-labelstringAccessible name for the trigger (recommended when there is no visible label).
calendarPropsOmit<CalendarProps, "disabled" | "mode" | "onSelect" | "selected">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.
classNamestringExtra classes for the trigger button.
disabledbooleanDisable the whole control.
disabledDatesMatcher[] | MatcherDates 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.
localestringBCP-47 locale for formatting (defaults to the runtime locale).
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.
valueDateRangeThe selected range (controlled).

Data attributes and CSS variables on DateRangePicker

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

Calendar

PropTypeDefaultDescription
refReact.Ref<HTMLDivElement>Ref to the calendar root element (data-slot="calendar"). DayPicker itself doesn't forward a consumer ref, so we wire it onto the overridden Root host alongside react-day-picker's own animation rootRef.
showOutsideDaysbooleantrueShow the outside days (days falling in the next or the previous month). **Note:** when a broadcastCalendar is set, this prop defaults to true. Render days from the adjacent months to fill the leading/trailing week rows.

Data attributes and CSS variables on Calendar

AttributeValues
data-slot"calendar"

CalendarDayButton

CalendarDayButton adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Data attributes and CSS variables on CalendarDayButton

AttributeValues
data-daymirrors a prop or state value
data-range-end""
data-range-middle""
data-range-start""
data-selected-single""
data-slot"calendar-day"
data-today""

DatePreset

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

DatePreset

PropTypeDefaultDescription
date*DateThe date this preset selects.
label*stringButton text, e.g. "Today".

DateRangePreset

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

DateRangePreset

PropTypeDefaultDescription
label*stringButton text, e.g. "Last 7 days".
range*DateRangeThe { from, to } range this preset selects.

Preset helpers

Ready-made preset rails. Pass your own presets to override either.

PropTypeDefaultDescription
defaultDatePresets(now?: Date) => DatePreset[]Today / Tomorrow presets for a single DatePicker. Accepts an optional reference date (defaults to now).
defaultRangePresets(now?: Date) => DateRangePreset[]Today / Last 7 days / Last 30 days presets for a DateRangePicker. Accepts an optional reference date (defaults to now).

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

  • Built on react-day-picker, which renders the month as a grid of gridcell day buttons with descriptive aria-labels ("Saturday, June 21st, 2026").
  • The selected day, range ends, today, and disabled days are all distinguishable by state — not by color alone — via data-* driven surfaces (bg-primary, ring, bg-accent, reduced opacity).
  • 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.
  • Focus is always visible: the outline Button trigger brightens its border on :focus-visible (focus-visible:border-ring/(--alpha-tint-border)), and the day grid manages roving focus itself (react-day-picker focuses the active day on keyboard navigation). Today's cell additionally carries a neutral ring (ring-ring/(--alpha-outline-soft)).
  • The trigger has no visible text label when empty beyond its placeholder, so pass aria-label (or wrap it in a Field) to name it for assistive tech.
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