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, onValueChange
Date | null, DateRange, "HH:mm" | null
presets, numberOfMonths
{label, range}[], 1 | 2 default , 2
disabledDays
(date) => boolean | {before, after}
weekStartsOn, locale, format
0 to 6, string, Intl options default 0, 'en-US', Sep 18, 2026
hourCycle, step, min, max
12 | 24, 5 to 60, "HH:mm" default 24, 15
clearable
boolean default false
size
'sm' | 'default' | 'lg' default 'default'
name
string

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)

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.