Date picker
Sets a date, a range or a time.
Granularity
datedefault
range
datetime
time
Sizes
sm
defaultdefault
lg
States
empty
hover
focus
picked range
invalid
disabled
Not a time. Try 9:30 or 21:00.
unreadable time
Usage
When to use it
- Use
- A date in the next or last few months: a snooze, a scheduled eval, when a key expires.
- Use
- A period for a report or a chart, where its length matters (mode="range").
- Use
- A time of day for something that repeats (granularity="time").
- Not for
- A date years away, like a birth date; type it. Use Input.
- Not for
- A few fixed periods: 24h, 7d, 30d. Use Segmented control.
- Not for
- A duration, like a timeout. Use Number input.
Rules
8 rules
- Must
- Use mode="range" when the length matters; never two pickers for a start and an end. Because two pickers let the end come before the start and lose the day count the range shows.
- Must
- Read the value from onValueChange on DatePicker, never from a bare Calendar's onSelect. Because the Calendar fires on the first click of a range, so a query runs with no end date; the picker writes only a finished pick.
- Must
- Pass disabledDays for days that cannot be picked; never reject a date after the pick. Because the person learns the date was never allowed only after choosing it, and the arrow keys cannot skip it.
- Must
- Put the picker in a Field with a FieldLabel, or give it aria-label. Because the trigger is a button whose text is the value, so without a label a screen reader hears only the date.
- Never
- Restyle the picked day with an ink fill or a ring in className. Because the picked day is the one pressed thing in the grid and takes small glaze like a ticked box, so a flat fill reads as another system.
- Should
- Name a preset "Last 7 days" and count it to include today.
- Should
- Set clearable on an optional date, so it can go back to empty.
- Never
- Add date-fns, dayjs or react-day-picker for it. Because the picker works on Date and Intl, so a library adds weight and a second idea of the week start and the locale.
State order
6 states, highest first
- 1
- disabled
- 2
- invalid
- 3
- open
- 4
- focus
- 5
- hover
- 6
- rest
Accessibility
5 checks
- The trigger is a button named by the label and the value together, with aria-expanded and aria-invalid.
- The calendar is an ARIA grid with one tab stop; days are named by their full date and carry aria-selected and aria-current="date".
- The picked day reads by its glaze and aria-selected; today by its hairline underline and "today" in its name.
- The footer's count and what to pick next sit in a status region, so a screen reader hears them.
- An unreadable time says why in words under the field, not only with the coral line.
Keyboard
5 keys
- Arrows
- Move by day and week, mirrored in right to left; disabled days are skipped.
- Page Up, Page Down
- Move by month; with Shift, by year.
- Home, End
- Move to the week's ends.
- Enter, Space
- Pick the focused day; in the time field, commit the typed time.
- Escape
- Close and drop the draft; the value stays as it was.
Edge cases
7 edge cases
- Narrow trigger
- The date truncates; the day count drops out of the trigger first.
- One-day range
- Reads "1 day", singular.
- Under 640px
- The range shows one month and the presets wrap above it.
- Same day clicked twice in a range
- A one-day range.
- Right to left
- The month arrows mirror and the band rounds at its inline ends.
- Inside a Dialog
- Escape closes the calendar first, then the dialog.
- Forced colors
- The picked day draws a 2px border; the band takes Highlight.
API
10 props
granularity'date' | 'datetime' | 'time'default'date'mode'single' | 'range'default'single'value, defaultValue, onValueChangeDate | null, DateRange, "HH:mm" | nullpresets, numberOfMonths{label, range}[], 1 | 2default, 2disabledDays(date) => boolean | {before, after}weekStartsOn, locale, format0 to 6, string, Intl optionsdefault0, 'en-US', Sep 18, 2026hourCycle, step, min, max12 | 24, 5 to 60, "HH:mm"default24, 15clearablebooleandefaultfalsesize'sm' | 'default' | 'lg'default'default'namestring
Tokens
33 tokens
--paper- #ffffff
--shell- #f1f1f0
--field- #efefef
--line-control- rgb(5 5 5 / 0.15)
--line-focus- rgb(5 5 5 / 0.32)
--line-faint- rgb(5 5 5 / 0.06)
--line- rgb(5 5 5 / 0.10)
--fail- #e35e49
--fail-text- #b23c29
--hover- rgb(5 5 5 / 0.06)
--pressed- rgb(5 5 5 / 0.10)
--flat-hover- #f8f8f8
--glaze-sm-top- #6b6b6b
--glaze-sm-mid- #3d3d3d
--glaze-sm-bottom- #232323
--glaze-sm-highlight- rgb(255 255 255 / 0.30)
--glaze-sm-rim- rgb(0 0 0 / 0.55)
--glaze-sm-text- rgb(255 255 255 / 0.92)
--text-1- rgb(5 5 5 / 0.80)
--text-2- rgb(5 5 5 / 0.60)
--text-3- rgb(5 5 5 / 0.45)
--text-disabled- rgb(5 5 5 / 0.32)
--control-sm- 26px
--control- 32px
--control-lg- 36px
--menu-row-sm- 28px
--shell-inset- 3px
--radius-shell- 14px
--radius-panel- 11px
--radius-pill- 999px
--radius-tag- 6px
--type-label- 500 13px/1.35 var(--font-sans)
--type-caption- 400 12px/1.35 var(--font-sans)