DatePicker
<DatePicker> is a single-date input — a Button trigger that opens a Popover containing the Calendar. Adapted from shadcn’s canonical pattern; uses react-day-picker v9 + date-fns.
<DateRangePicker> is the same shape with mode="range".
When to use
- Any structured date field — birth date, due date, expiry, scheduled run.
- Date-range filters in list / report views.
Don’t use DatePicker for: free-text date strings (use Input only when the date is part of a sentence), time-of-day pickers (separate primitive — not yet planned), or relative-date selection (“in 3 days”) — provide presets in a DateRangePicker for that.
Single date
const [d, setD] = useState<Date | undefined>();
<DatePicker value={d} onChange={setD} placeholder="Pick a date" />Disabled days
Pass a callback returning true for days that can’t be selected — for business-rule blocking.
<DatePicker value={d} onChange={setD} disabledDays={(date) => date.getDay() === 0 || date.getDay() === 6}/>Date range
const [r, setR] = useState<DateRange | undefined>();
<DateRangePicker value={r} onChange={setR} placeholder="Pick a range" />API
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | — | Controlled date. Pair with onChange. |
onChange | (date: Date | undefined) => void | — | Fires on selection. undefined when the user clears. |
placeholder | string | "Pick a date" | Trigger label when there is no value. |
formatStr | string | "MMM d, yyyy" | date-fns format token. |
locale | Locale | — | date-fns locale for the trigger label. |
disabledDays | (date: Date) => boolean | Matcher | Matcher[] | — | Forwarded to react-day-picker for per-day disabling. |
disabled | boolean | false | Disable the trigger entirely. |
align | "start" | "center" | "end" | "start" | Popover anchor alignment. |
className | string | — | Forwarded to the trigger Button. |
contentClassName | string | — | Forwarded to the PopoverContent. |
DateRangePicker
Same as DatePicker, with:
| Prop | Type | Default | Description |
|---|---|---|---|
value | DateRange | — | `{ from?: Date; to?: Date }`. Controlled. |
onChange | (range: DateRange | undefined) => void | — | Fires when from or to changes. |
numberOfMonths | number | 2 | Months rendered side-by-side. |
Design guidelines
✓ Do
- Always pair with a Label or aria-label. Standalone DatePickers have no name.
- Set disabledDays for business rules — past dates for future-only events, weekends for business days, etc.
- Use DateRangePicker for filters. Single-date filters are usually a smell — most date filters are ranges.
✗ Don't
- Use DatePicker for free-text dates (typed inline). The picker IS the input.
- Override formatStr with locale-incompatible tokens. date-fns format quietly produces "wrong" output if you mix.
- Hide the trigger label when no value is set. The placeholder is the only signal of "this is a date field" before opening.
Accessibility
- Composes Radix Popover under the hood — focus trapped while open, Esc closes.
- The Calendar is fully keyboard-navigable: ←/→/↑/↓ between days, PageUp/PageDown between months, Enter selects.
- The trigger reflects the selected value as its accessible name.
- Set
aria-labelon the trigger when there’s no surrounding<Label>.