Date picker
NewA field that opens a calendar: one day (and a time, if it matters) with DatePicker, or a first and last day with DateRangePicker. It looks and sizes exactly like Input, so dates sit in forms beside everything else.
Playground
Change the props, then open the calendar with a click, or with Enter or Space once the field has focus. A day pick closes it; with granularity="minute" it stays open for the time.
<DatePicker aria-label="Delivery date" />Picking a day
Put the picker in a Field for its label, hint and error, as with any input. Here a delivery can be booked from tomorrow, on any day but Sunday: minValue rules out the past (and the months before it cannot even be reached), and disabledDays rules out Sundays. format adds the weekday to the field, because for a delivery the day of the week matters.
Press Book delivery with no date to see the error, and focus come back to the field.
| Part | What it does |
|---|---|
| Field | The Input's well: same edge, halo, heights and error look. While the calendar is open it keeps its teal edge, so the two read as one. |
| Calendar glyph | Quiet slate, firming up while the field has focus. Clicking anywhere on the field opens the calendar. |
| Date | Formatted with Intl in British English: 2 Oct 2026 by default, the placeholder in quiet slate until then. |
| Clear button | With clearable, a small round button pops in at the end once there is a date. |
| Calendar | On the Popover's sheet, growing out of the field. See Calendar for the grid itself. |
Sizes
sm 30px, md 36px (default) and lg 44px: the heights of Input and Button, so a date, a text field and a button share a line exactly.
States
Hover firms the edge; focus turns it teal with a soft halo (Tab into a field to see it). An error swaps the teal for red. Read-only shows the date in the well and will not open; disabled dims it and Tab skips it.
Clearing
An optional date needs a way back to none. clearable adds a round clear button at the end of the field once there is a date, and Backspace or Delete on the field clears it too. The button stays out of the Tab order, as the search field's does. A required date leaves it off.
Date and time
granularity="minute" adds a time row under the calendar: hours and minutes on a 24-hour clock, as UK shops expect. Picking a day keeps the calendar open so the time can follow. Click into the hours and type "9": it can only mean 09, so focus moves on to the minutes. The arrow keys step either part, and Enter closes.
Picking a new day always keeps the time the value had, even without a time row, so moving a 16:30 deadline to Friday leaves it at 16:30. With a time, minValue and maxValue hold to the minute.
12-hour clock
hourCycle={12} shows the hours 1 to 12 with an AM and PM switch beside them, and the field reads 9:00 am. Keep the default 24-hour clock unless the people using it expect otherwise.
A start and an end
When the two ends each have a time, or the end is optional, as with a discount, use two pickers and let the start bound the end: minValue={starts} on the end keeps it from being picked before the start. Move the start past the end to see the error.
Date ranges
DateRangePicker picks a run of whole days: the first click starts the range, the second ends it and closes the calendar. While the end is being picked, a fainter wash follows the pointer (or focus) to a teal ring. Picking an earlier day second simply swaps the ends. Two months sit side by side on screens 768px and wider, one below.
The value is { from, to }, both local midnight and both included, and the field reads 2 Oct 2026 to 9 Oct 2026.
Presets
presets adds a column of ready-made ranges on the left, as reports need: Today, Last 7 days, Last 30 days, This month and Last month. Picking one applies it at once. When the range matches a preset, the field names it ("Last 30 days") and the preset carries a teal check; any other range shows its dates.
onChange gets the preset's id as a second argument, so a report can keep "Last 30 days" rolling forward day by day rather than freezing the dates. On phones the presets become a row of chips above one month.
Your own presets
Pass a list of { id, label, range }, where range(today) returns the dates. These match the analytics pages. Presets that would cross minValue or maxValue are shown but cannot be picked.
Limits and unavailable days
Both pickers take the same limits. Unavailable days stay visible, in mist, so the month keeps its shape, and cannot be picked or focused. The Calendar page lists every way to describe them.
| Prop | Rules out | For example |
|---|---|---|
minValue | Every day before it. Months before it cannot be reached. | Delivery from tomorrow; a discount end no earlier than its start. |
maxValue | Every day after it. Months after it cannot be reached. | Reports up to today: maxValue={today}. |
disabledDays | Any day matching: a date, dates, { dayOfWeek }, a range, or a function. | Sundays: { dayOfWeek: [0] }. Bank holidays: (date) => isBankHoliday(date). |
Keyboard
Everything works from the keyboard: open, move, pick and close, and focus always lands somewhere useful.
| Where | Key | What it does |
|---|---|---|
| Field | Enter, Space, Down arrow | Opens the calendar, with focus on the chosen day (or today, or the first day that can be picked). |
| Field | Backspace, Delete | Clears the date, when the picker is clearable. |
| Calendar | Arrow keys | Move a day, or a week up and down, into the next month when they run off the edge. |
| Calendar | Page Up, Page Down | Move a month. With Shift, a year. |
| Calendar | Home, End | Move to the start or the end of the week. |
| Calendar | Enter, Space | Pick the day. A single day closes the calendar; a range closes on its second day. |
| Time | Digits, Up and Down arrows | Type the hours and minutes, or step them round the clock. |
| Time | Enter | Closes the calendar. |
| Anywhere inside | Tab, Shift Tab | Move between the presets, the month buttons, the grid and the time, and round again: focus stays inside while it is open. |
| Anywhere inside | Escape | Closes the calendar and returns focus to the field. A range with only its first day picked is dropped. |
Replacing a native date field
The pickers take and give a Date. A native <input type="date"> holds a string,YYYY-MM-DD, and new Date() reads that as midnight UTC, which is still the day before anywhere west of Greenwich. Convert at the edge with the helpers, which read and write the local day and never shift it.
Pick a day, then clear it, and watch the stored string.
| Native | DatePicker |
|---|---|
type="date" | DatePicker. The value is a Date at local midnight. |
type="datetime-local" | granularity="minute", with toISODateTime for the YYYY-MM-DDTHH:mm string. |
value | value={fromISODate(text)}. Empty or invalid text reads as null. |
onChange | onChange={(date) => set(date ? toISODate(date) : '')}. Called with a Date, or null when cleared. |
min, max | minValue, maxValue (pass fromISODate(min)). |
disabled, readOnly | The same. |
required | Leave clearable off and check on submit, showing error on the Field. |
name | The same: submits YYYY-MM-DD (or YYYY-MM-DDTHH:mm) with the form. |
| Two date fields for a range | One DateRangePicker, unless each end needs its own time or the end is optional. |
| Helper | Does |
|---|---|
toISODate(date) | '2026-10-02': the local calendar day, as a date input holds it. |
toISODateTime(date) | '2026-10-02T09:30': local time, as a datetime-local input holds it. |
fromISODate(text) | Reads both of those as local time, and a full timestamp with a zone (…Z, +01:00) as the instant it is. Empty, malformed or impossible dates give null. |
In both themes
Held open for review, light beside dark: the field keeps its teal edge while the calendar is open, and the sheet is white in light and graphite in dark. The pickers on the rest of this page are live.
Guidelines
- Label the field with what the date is for ("Delivery date", "Publish on"), never just "Date". End an optional one's label with "(optional)" and make it
clearable. - Rule out what cannot be picked with
minValue,maxValueanddisabledDays, then say why in the hint ("We deliver Monday to Saturday"). - Add the weekday to the format when the day of the week matters to the decision, as it does for deliveries and bookings.
- Keep the 24-hour clock and day, month, year order: they are what UK merchants read without thinking.
- Use a SegmentedControl, not a picker, for a handful of fixed periods that change a view in place (Today, 7 days, 30 days).
Accessibility
- The field is a button with
aria-haspopup="dialog"andaria-expanded, named by the Field's label and the date together ("Delivery date, Fri, 9 Oct 2026") througharia-labelledby, so the current value is always read. Without a Field, give it anaria-label. - Hints and errors are linked with
aria-describedby, and an error setsaria-invalid. - The calendar opens in a dialog named after the field. It is a
gridof day buttons, each read in full ("Today, Friday, 2 October 2026, selected"); the month caption is a polite live region, so moving month is announced. - Focus moves to the chosen day when the calendar opens, stays inside while it is open, and goes back to the field when it closes.
- Unavailable days are disabled buttons: they cannot be focused or picked, and are skipped by the arrow keys.
- The time parts are spin buttons named Hours and Minutes, with their value and range.
- With reduced motion, the sheet fades in without growing, the chosen-day disc appears without settling, and months swap without sliding.
Props
Both pickers forward every other button attribute, and the ref, to the field's button. className and style go to the well around it.
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | null | None | Controlled value: a local date, with a time when granularity is minute. |
defaultValue | Date | null | None | Starting value when uncontrolled. |
onChange | (value: Date | null) => void | None | A day picked, a time changed or the field cleared. Never on mount. |
granularity | 'day' | 'minute' | 'day' | Minute adds hours and minutes under the calendar. |
hourCycle | 12 | 24 | 24 | The clock for the time and the field. |
size | 'sm' | 'md' | 'lg' | 'md' | 30, 36 or 44px: the same heights as Input and Button. |
placeholder | string | 'Choose a date' | Shown while there is no date ('Choose a date and time' with a time). |
clearable | boolean | false | A clear button, and Backspace or Delete, while there is a date. |
minValue | Date | null | None | The earliest day (and time, with a time) that can be picked. |
maxValue | Date | null | None | The latest day (and time) that can be picked. |
disabledDays | CalendarMatcher | CalendarMatcher[] | None | Days that cannot be picked: Sundays, bank holidays, sold-out days. |
format | Intl.DateTimeFormatOptions | None | How the field shows the date. Defaults to 2 Oct 2026, and 2 Oct 2026, 09:30 with a time. |
locale | string | 'en-GB' | Locale for the field, the month and day names, and the first day of the week. |
weekStartsOn | 0 to 6 | None | First column of the week, 0 for Sunday. Defaults to the locale's: Monday in en-GB. |
invalid | boolean | None | Red edge and aria-invalid. Inside a Field, follows the Field's error. |
readOnly | boolean | None | Shows the date in the well but will not open. |
disabled | boolean | None | Dims the field; Tab skips it. |
closeOnSelect | boolean | true | Close when a day is picked. False by default with a time. |
name | string | None | Submits YYYY-MM-DD (or YYYY-MM-DDTHH:mm) with a form. |
open, defaultOpen, onOpenChange | boolean, boolean, (open) => void | None | Control the calendar yourself. |
modal | boolean | true | Keep focus and the pointer inside the calendar while it is open. |
align, side | 'start' | 'center' | 'end', 'bottom' | 'top' | … | 'start', 'bottom' | Where the calendar opens. It flips when there is no room. |
container | HTMLElement | null | document.body | Where the calendar is portalled. |
today | Date | None | The day treated as today. Defaults to now. |
labels | Partial<DatePickerLabels> | None | Translations: clear, time, hours, minutes, dayPeriod, previousMonth, nextMonth, today, selected. |
DateRangePicker (where it differs)
| Prop | Type | Default | Description |
|---|---|---|---|
value | DateRange | null | None | Controlled range: { from, to }, both local midnight and both included. |
defaultValue | DateRange | null | None | Starting range when uncontrolled. |
onChange | (value: DateRange | null, preset?: string) => void | None | Once a range is complete, a preset is picked (with its id), or the field is cleared. |
presets | boolean | DateRangePreset[] | false | True for dateRangePresets (Today, Last 7 days, Last 30 days, This month, Last month), or your own list. |
numberOfMonths | number | None | Months side by side. Defaults to two from 768px wide, one below. |
placeholder | string | 'Choose dates' | Shown while there is no range. |
labels | Partial<DateRangePickerLabels> | None | Translations: clear, to (between the dates), presets, and the calendar labels. |
DateRangePicker has no time and no granularity, hourCycle or name; the rest is the same as DatePicker.
DateRangePreset
| Prop | Type | Default | Description |
|---|---|---|---|
idRequired | string | None | Passed to onChange when the preset is picked. |
labelRequired | string | None | Shown in the list, and in the field when the range matches. |
rangeRequired | (today: Date) => DateRange | None | The dates for a given today (local midnight). |