Forms
DatePicker
A date — or a span of them — chosen from a calendar.
When to reach for it
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
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | Names the control. Required — the trigger's text is a value, not a label. | |
| className | string | — | |
| defaultValue | Date | — | |
| disabled | boolean | false | — |
| disabledDates | ComponentProps<typeof Calendar>['disabled'] | Days the reader may not choose. Passed straight to the calendar. | |
| format | (date: Date) => string | formatDate | How the chosen date is printed on the trigger. |
| onValueChange | (value: Date | undefined) => void | — | |
| placeholder | string | 'Pick a date' | — |
| presets | boolean | 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. | |
| value | Date | — |
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | — | |
| className | string | — | |
| defaultValue | DateRange | — | |
| disabled | boolean | false | — |
| disabledDates | ComponentProps<typeof Calendar>['disabled'] | — | |
| format | (date: Date) => string | formatDate | — |
| months | number | 2 | How many months are shown side by side. Falls back to one under `sm`. |
| onValueChange | (value: DateRange | undefined) => void | — | |
| placeholder | string | 'Pick a range' | — |
| presets | boolean | DatePreset<DateRange>[] | true | Shortcuts 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. |
| value | DateRange | — |
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
| Key | Does |
|---|---|
| EnterSpace | Opens the calendar. |
| Escape | Closes 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.