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-labelon 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
disabledmatcher to block unavailable dates — e.g.{ before: new Date() }for future-only inputs. - Don't omit
autoFocusinside 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-labelreflecting the current selection — the icon alone is not sufficient for screen readers. PopoverContentopens withrole="dialog". PassautoFocustoCalendarso 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-startanddata-range-endattributes on day buttons.
Prop Reference
| Prop | Component | Purpose |
|---|---|---|
value | DatePicker | Controlled selected date (Date | undefined) |
onValueChange | DatePicker | Callback when the selected date changes |
placeholder | DatePicker | Trigger text when no date is selected (default: "Pick a date") |
disabled | DatePicker | Disables the trigger button |
value | DateRangePicker | Controlled selected range (DateRange | undefined) |
onValueChange | DateRangePicker | Callback when the selected range changes |
presets | DatePickerWithPresets | Array of { label, days } shortcuts above the calendar |
autoFocus | Calendar | Moves DOM focus into the calendar grid on mount |
captionLayout | Calendar | "label" (default) | "dropdown" — adds month/year selects |
disabled | Calendar | Matcher 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>