Skip to content
GitHub

Forms

DatePicker

A date — or a span of them — chosen from a calendar.

When to reach for it

Deliberately not a text input with a calendar attached: parsing a typed date needs a format, and 03/04 is March the fourth in one country and the third of April in the next. When the date is a long way back, the calendar’s month and year are dropdowns.

Examples

default

Month and year are dropdowns — reaching two years back is one click, not twenty-four.

range

Last 30 days and its neighbours are one click; the grid is for everything else.

presets

Shortcuts are computed when clicked, so “today” means today even on a tab left open overnight.

Notes

A date, chosen from a calendar.

A trigger and a Calendar in a Popover — not a new component so much as the composition people otherwise assemble slightly differently on every screen.

It is deliberately NOT a text input with a calendar attached. A typed date needs parsing, and parsing needs a format, and a format is a locale argument nobody wins. When typing genuinely matters — a birth date, a long way back — the calendar's month and year are dropdowns, which is the same journey without the ambiguity.

Props

DatePicker props
PropTypeDefaultDescription
labelrequiredstringNames the control. Required — the trigger's text is a value, not a label.
classNamestring
defaultValueDate
disabledbooleanfalse
disabledDatesComponentProps<typeof Calendar>['disabled']Days the reader may not choose. Passed straight to the calendar.
format(date: Date) => stringformatDateHow the chosen date is printed on the trigger.
onValueChange(value: Date | undefined) => void
placeholderstring'Pick a date'
presetsboolean | DatePreset<Date>[]Shortcuts shown beside the grid. Pass `true` for the built-in set, an array for your own, or leave it off for none.
valueDate

Parts

Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.

DateRangePicker

A span of dates — a stay, a reporting period, a filter.

Two months side by side, because a range that crosses a month boundary is the common case, and paging back and forth to see both ends is what makes a range picker tiring. They stack under sm, where two would not fit — the calendar's own months class already carries that, so there is nothing to override.

The panel stays open until both ends are chosen: a range is not a value until it has a second date, and closing on the first one would mean re-opening to finish.

DateRangePicker props
PropTypeDefaultDescription
labelrequiredstring
classNamestring
defaultValueDateRange
disabledbooleanfalse
disabledDatesComponentProps<typeof Calendar>['disabled']
format(date: Date) => stringformatDate
monthsnumber2How many months are shown side by side. Falls back to one under `sm`.
onValueChange(value: DateRange | undefined) => void
placeholderstring'Pick a range'
presetsboolean | DatePreset<DateRange>[]trueShortcuts shown beside the grid — Last 30 days and its neighbours. `true` for the built-in set, an array for your own. On by default here and off on the single picker, because "last 30 days" is most of what a range picker is ever asked for, while a single date is usually a specific one.
valueDateRange

Re-exports

RANGE_PRESETS = [ { label: 'Last 7 days', value: () => daysAgo(7) }, { label: 'Last 30 days', value: () => daysAgo(30) }, { label: 'Last 90 days', value: () => daysAgo(90) }, { label: 'Last 12 months', value: () => daysAgo(365) }, { label: 'Month to date', value: () => { const to = new Date() return { from: new Date(to.getFullYear(), to.getMonth(), 1), to } }, }, { label: 'Year to date', value: () => { const to = new Date() return { from: new Date(to.getFullYear(), 0, 1), to } }, }, ]

The shortcuts a range picker is asked for on nearly every screen it appears on, so they ship rather than being rebuilt per dashboard.

Computed on click: a preset list built at render time freezes "today" at whenever the page loaded, which is wrong for anything left open overnight.

DATE_PRESETS = [ { label: 'Today', value: () => new Date() }, { label: 'Tomorrow', value: () => { const date = new Date() date.setDate(date.getDate() + 1) return date }, }, { label: 'In a week', value: () => { const date = new Date() date.setDate(date.getDate() + 7) return date }, }, { label: 'In a month', value: () => { const date = new Date() date.setMonth(date.getMonth() + 1) return date }, }, ]

The single-date equivalent.

Keyboard

DatePicker keyboard interactions
KeyDoes
EnterSpaceOpens the calendar.
EscapeCloses it without choosing.

Accessibility

  • The trigger prints the date in the visitor’s own locale, not a fixed dd/mm/yyyy.
  • DateRangePicker keeps the panel open until both ends are chosen — a range is not a value until it has a second date.
  • The shortcut rail is plain buttons, not a menu: they set the same value the grid beside them sets, so they belong to one control and Tab in the same pass.
  • Presets are computed on click, so “today” means today even on a tab left open overnight.