DatePicker

A date picker pattern composed from Popover, Button, and Calendar. Supports single date selection, date ranges, date-of-birth dropdowns, and quick-select presets.

Anatomy

A <Popover> wraps a <PopoverTrigger> rendered as a <Button> and a <PopoverContent> containing a <Calendar>. The trigger displays the formatted selection or a placeholder when no date is selected.

Variants

Basic

Single date via mode="single".

Range

Start and end date via mode="range" with two months visible.

Date of Birth

Dropdown navigation via captionLayout="dropdown" for fast decade navigation.

With Presets

Quick-select shortcuts (Today, Tomorrow, In 7 days, In 30 days) above the calendar grid.

States

Default (no selection)

With selection

Disabled

Design Guidelines

Do

  • Show the selected value in the trigger. Format it clearly (e.g. “Jan 20, 2025”) so users can confirm their choice without reopening the popover.
  • Use dropdown navigation for date of birth. captionLayout="dropdown" prevents users from clicking through decades of months one at a time.
  • Add presets for relative dates. Shortcuts like “Today” and “In 7 days” reduce clicks for common scheduling scenarios.
  • Provide an accessible trigger label. Use aria-label on the trigger button to announce both the current value and role to screen readers.

Don't

  • Don't use a date picker for well-known past dates. For fixed historical dates, a masked text input (MM/DD/YYYY) is faster than calendar navigation.
  • Don't skip disabled date constraints. Pass a disabled matcher to block unavailable dates — e.g. { before: new Date() } for future-only inputs.
  • Don't omit autoFocus inside a popover. Without it, keyboard users must tab multiple times to reach the calendar grid after opening the popover.

Developer Reference

Accessibility

  • The trigger button must have an aria-label reflecting the current selection — the icon alone is not sufficient for screen readers.
  • PopoverContent opens with role="dialog". Pass autoFocus to Calendar so focus moves into the grid immediately on open.
  • Arrow keys navigate between days; Page Up / Page Down change months; Home / End jump to the start or end of the week.
  • ESC closes the popover and returns focus to the trigger button.
  • Range endpoints are communicated via data-range-start and data-range-end attributes on day buttons.

Prop Reference

PropComponentPurpose
valueDatePickerControlled selected date (Date | undefined)
onValueChangeDatePickerCallback when the selected date changes
placeholderDatePickerTrigger text when no date is selected (default: "Pick a date")
disabledDatePickerDisables the trigger button
valueDateRangePickerControlled selected range (DateRange | undefined)
onValueChangeDateRangePickerCallback when the selected range changes
presetsDatePickerWithPresetsArray of { label, days } shortcuts above the calendar
autoFocusCalendarMoves DOM focus into the calendar grid on mount
captionLayoutCalendar"label" (default) | "dropdown" — adds month/year selects
disabledCalendarMatcher to mark dates as unselectable

Usage

import { DatePicker, DateRangePicker, DatePickerWithPresets } from "@/components/ui/date-picker"

// Uncontrolled single date
<DatePicker placeholder="Pick a date" />

// Controlled single date
const [date, setDate] = useState<Date | undefined>(undefined)
<DatePicker value={date} onValueChange={setDate} />

// Date range
const [range, setRange] = useState<DateRange | undefined>(undefined)
<DateRangePicker value={range} onValueChange={setRange} />

// With quick-select presets
<DatePickerWithPresets />

// Custom presets
<DatePickerWithPresets
  presets={[
    { label: "Today", days: 0 },
    { label: "Next week", days: 7 },
    { label: "Next month", days: 30 },
  ]}
/>

// Disabled
<DatePicker disabled />

// ── Building from primitives (for custom layouts) ──────────────────────────
import { useState } from "react"
import { format } from "date-fns"
import { CalendarIcon } from "lucide-react"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"

const [date, setDate] = useState<Date | undefined>(undefined)

<Popover>
  <PopoverTrigger
    render={
      <Button
        variant="outline"
        aria-label={date ? format(date, "PPP") : "Pick a date"}
        className="w-[240px] justify-start gap-2 font-normal"
      />
    }
  >
    <CalendarIcon className="size-4 text-muted-foreground" />
    {date
      ? <span>{format(date, "PPP")}</span>
      : <span className="text-muted-foreground">Pick a date</span>
    }
  </PopoverTrigger>
  <PopoverContent className="w-auto p-0">
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
      disabled={{ before: new Date() }}
      autoFocus
    />
  </PopoverContent>
</Popover>