Skip to content

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

import from "@na/ui/components/DatePicker" ▶ Open in Storybook packages/ui/src/components/DatePicker.tsx

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-label on the trigger when there’s no surrounding <Label>.
  • Calendar — Underlying primitive for in-page calendars.
  • Combobox — Searchable single-select for non-date pickers.
  • Form — Spread the field onto DatePicker for react-hook-form integration.

▶ Open DatePicker stories in Storybook