Date picker

Sets a date, a range, a date and time, or a time of day.

Examples

A report period

  • Last 7 days
  • Last 14 days
  • Last 30 days
September 2026
SuMoTuWeThFrSa

7 days

When a key expires

October 2026
SuMoTuWeThFrSa

Snooze with no past days

Quiet hours

Stored as 02:30, 24-hour.

Granularity

  • datedefault

  • range

  • datetime

  • Stored as 02:30, 24-hour.

    time

Sizes

  • sm

  • defaultdefault

  • lg

States

  • empty

  • hover

  • focus

  • half a range

  • invalid

  • disabled

  • Not a time. Try 9:30 or 21:00

    unreadable time

Usage

When to use it
Use
A date that could be any day in the next or last few months: a snooze, a scheduled eval.
Use
A period for a report or chart, where the length matters.
Use
A time of day for something that repeats, like nightly quiet hours.
Not for
A date of birth or anything years away; type it. Use Input.
Not for
A handful of fixed periods (24h, 7d, 30d). Use Segmented control.
Not for
A duration, like a timeout; use a number input with a unit. Use Input.

Rules

9 rules
Must
Use mode="range" when the length matters; never two separate pickers. Because two pickers let the end come before the start and lose the day count the range shows.
Must
Write a range or datetime value only on a finished pick: both ends, a preset or Set. Because the first click writes half a range, and a query runs with no end date.
Must
Pass disabledDays for days that cannot be picked; never validate after the pick. Because the person learns the date was never allowed only after picking it; disabled days are skipped by the arrow keys.
Should
Let the person type a time and keep step at 15 or 30 for the list.
Should
Name a preset 'Last 7 days' and count it to include today.
Must
Put a labelled picker in a Field with a FieldLabel. Because the trigger is a button that takes its name from the FieldLabel, so a p leaves it unnamed.
Should
Set clearable on an optional date so it can go back to empty.
Never
Add date-fns, dayjs or react-day-picker; this picker needs only Date and Intl. Because the picker works on plain Date and Intl, so a library adds bundle weight and a second idea of week start and locale.
Should
Expect one month under 640px; do not force numberOfMonths={2} in a narrow sheet.

Accessibility

6 checks
The trigger is a button named by the Field label plus the value, with aria-expanded and aria-invalid.
The calendar is an ARIA grid; days are named by their full date and carry aria-selected and aria-current="date".
Arrows move by day and week, Page Up and Down by month, with Shift by year, Home and End by week; left and right swap in right to left.
Escape closes and drops the draft; the value stays as it was.
The range caption announces what to pick next and the day count.
An unreadable time shows its reason in visible text under the field.

Edge cases

12 edge cases
Long German date in 320px
Truncates in the trigger; the day count drops out first.
One-day range
Reads "1 day", singular.
No value, clearable
The placeholder shows; the popup ends with Clear.
160px wide range
The trigger fills the width; the popup drops to one month with wrapping presets.
Right to left
Carets mirror; the range band rounds its inline ends.
Zoom 200%
Day cells stay 32px; the popup scrolls inside the available height.
Text spacing
Two-letter weekdays and two-digit days still fit.
Date not allowed
Keeps the value, turns invalid and says how to fix it.
Same day clicked twice in range mode
Makes a one-day range.
Inside a dialog
Escape closes the calendar first, then the dialog.
Forced colors
Selected ends draw a 2px border; the band takes Highlight; today keeps its underline.
Reduced motion
The popup opens without a transition; month changes are cuts.

API

14 props
granularity
'date' | 'datetime' | 'time' default 'date'
mode
'single' | 'range' default 'single'
value, defaultValue, onValueChange
Date | null, DateRange, "HH:mm" | null
presets
{label, range}[]
disabledDays
(date) => boolean | {before, after}
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6 default 0
locale
string default 'en-US'
format
Intl.DateTimeFormatOptions default Sep 18, 2026
hourCycle
12 | 24 default 24
step, min, max
5 | 10 | 15 | 30 | 60, "HH:mm" default 15
name
string
clearable
boolean default false
size
'sm' | 'default' | 'lg' default 'default'
align
'start' | 'center' | 'end' default 'start'

Tokens

23 tokens
--line-control
oklch(var(--ink-c) / 0.16)
--line-frame
oklch(var(--ink-c) / var(--line-frame-alpha))
--accent-wash
var(--surface-selected)
--accent-mark
var(--ink)
--line-focus
2px
--focus-halo
0 0 0 3px oklch(var(--ink-c) / 0.08)
--border-focus
var(--ink)
--focus-offset
2px
--surface-page
var(--paper)
--radius-soft
6px
--elev-overlay
0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--dur-base
160ms
--gutter-sm
16px
--control
32px
--surface-hover
oklch(var(--ink-c) / 0.05)
--dur-fast
120ms
--button-primary
var(--ink)
--text-inverse
var(--paper)
--surface-pressed
oklch(var(--ink-c) / 0.08)
--text-3
oklch(var(--ink-c) / 0.66)
--text-disabled
oklch(var(--ink-c) / 0.40)
--type-label-sm
400 12px/16px var(--font-sans)
--text-2
oklch(var(--ink-c) / 0.80)

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.