Loading…
Loading…
Allows users to select a date or date range through a calendar interface.
The Date Picker (also called a calendar, date input, or date selector) is one of the most complex interactive components in UI design. It allows users to select a single date, a date range, or a date-time combination through a calendar interface, a text input, or both.
Date pickers sit at the intersection of several hard problems: internationalization (date formats, calendars, first-day-of-week), timezone handling, accessibility (navigating a grid of 42 cells by keyboard), validation (min/max dates, disabled dates, business rules), and responsive design (calendars are inherently wide).
When to use a Date Picker:
When NOT to use a Date Picker:
MM/DD/YYYY) may be faster. Offer both input + calendar.type="time").The best date picker implementations offer dual input: a text field where users can type a date directly, plus a calendar popover for visual selection. This serves both power users (who type faster) and visual users (who want to see context).
Ensure your calendar's date numbers and navigation icons pass contrast requirements — use our Contrast Checker. Style your calendar's selected-date highlight with the Color Palette Generator.
| Variant | Description | Use Case |
|---|---|---|
| Single date | Select one specific date | Birth date, deadline, appointment |
| Date range | Select a start and end date | Hotel booking, reporting period, leave request |
| Multi-date | Select multiple non-contiguous dates | Scheduling recurring events, marking exceptions |
| Date-time | Date + time picker combined | Meeting scheduler, event creation, timestamps |
| Month picker | Select a month and year (no day) | Credit card expiry, monthly reports |
| Year picker | Select a year only | Historical data filtering, birth year |
| Variant | Description |
|---|---|
| Input + Popover | A text input with a calendar icon that opens a popover calendar. Default and most common. |
| Inline calendar | Calendar is always visible, embedded in the page. No popover. For booking/scheduling UIs. |
| Dual calendar | Two months displayed side-by-side. Essential for date range selection. |
| Input-only | Text input with date masking, no calendar. For known dates (birth date). |
| Size | Input Height | Calendar Width | Use Case |
|---|---|---|---|
| Small | 32px | 280px | Dense forms, inline filters |
| Medium | 40px | 320px | Default forms |
| Large | 48px | 360px | Mobile-first, booking interfaces |
| Property | Type | Default | Description |
|---|---|---|---|
value | Date | null | null | Selected date (controlled) |
onChange | (date: Date | null) => void | — | Callback when date changes |
defaultValue | Date | — | Initial date (uncontrolled) |
minDate | Date | — | Earliest selectable date |
maxDate | Date | — | Latest selectable date |
disabledDates | (date: Date) => boolean | — | Function to disable specific dates (holidays, unavailable days) |
locale | string | Browser locale | BCP 47 locale string ('en-US', 'de-DE', 'ja-JP') |
firstDayOfWeek | 0-6 | Locale default | 0 = Sunday, 1 = Monday |
format | string | Locale default | Display format ('MM/dd/yyyy', 'dd.MM.yyyy') |
placeholder | string | Format string | Input placeholder text |
required | boolean | false | Form validation required |
disabled | boolean | false | Disables the entire picker |
readOnly | boolean | false | Allows viewing the calendar but not changing the value |
error | string | — | Error message |
clearable | boolean | false | Shows a clear button to reset the value |
| Property | Type | Default | Description |
|---|---|---|---|
startDate | Date | null | null | Range start |
endDate | Date | null | null | Range end |
onRangeChange | (range: { start: Date; end: Date }) => void | — | Callback for range changes |
minLength | number | — | Minimum range length in days |
maxLength | number | — | Maximum range length in days |
| Token Category | Token Example | Date Picker Usage |
|---|---|---|
| Color – Input | --color-surface, --color-border | Input field background and border |
| Color – Calendar Surface | --color-surface-elevated | Calendar popover background |
| Color – Selected | --color-primary-600 | Selected date circle fill. Choose accessible hues with Color Palette Generator. |
| Color – Selected Text | --color-on-primary | Text on the selected date |
| Color – Range | --color-primary-100 | Background highlight for dates within a selected range |
| Color – Today | --color-primary-600 (outline) | Today's date indicator (typically a ring or dot) |
| Color – Disabled | --color-text-disabled | Dates outside the selectable range |
| Color – Hover | --color-surface-hover | Date cell hover background |
| Color – Adjacent Month | --color-text-tertiary | Dates from previous/next month (dimmed) |
| Border Radius | --radius-full (9999px) | Selected date circle |
| Border Radius – Calendar | --radius-xl (16px) | Calendar popover corners |
| Shadow | --shadow-lg | Calendar popover elevation |
| Spacing | --space-1 (4px) | Gap between date cells |
| Typography | --font-size-sm, --font-variant-numeric: tabular-nums | Date numbers (tabular for alignment) |
See our Design Tokens Complete Guide for naming conventions.
| State | Visual Treatment |
|---|---|
| Empty | Placeholder showing the expected format (e.g., "MM/DD/YYYY") |
| Filled | Formatted date displayed in the input |
| Focused | Input border highlighted. Calendar popover opens (optional — some implementations open on icon click only). |
| Error | Red border, error message below. Invalid date entered or required field empty. |
| Disabled | Greyed out input and calendar icon. Not interactive. |
| State | Visual Treatment |
|---|---|
| Idle | Current month displayed. Today highlighted with subtle indicator (dot or ring). |
| Date hover | Background highlight on hovered date cell |
| Date selected | Solid circle (primary color) on the selected date. White text. |
| Range: start/end | Solid circles on start and end dates |
| Range: between | Light primary background connecting start and end dates |
| Range: preview | During selection (start chosen, hovering for end), show a preview of the range with lighter styling |
| Disabled date | Muted text, no hover effect, not selectable. Use aria-disabled="true". |
| Adjacent month | Dates from previous/next month shown in muted text at the edges of the grid |
| Month/Year navigation | Prev/Next arrows (◀ ▶) for month navigation. Month/year header may be clickable for month/year picker views. |
| View | Description |
|---|---|
| Day view | Default. 6×7 grid of dates for one month. |
| Month view | 3×4 grid of months. User picks a month, returns to day view. |
| Year view | Grid of years (e.g., 2020–2035). User picks a year, goes to month view, then day view. |
| Decade view | Range of decades for far-past/future date selection. |
| Criterion | Level | Requirement |
|---|---|---|
| SC 1.3.1 Info and Relationships | A | Calendar grid must use role="grid" with proper role="row" and role="gridcell" structure. Day-of-week headers need role="columnheader". |
| SC 1.4.3 Contrast (Minimum) | AA | Date numbers: 4.5:1 against cell background. Selected date text: 4.5:1 against the primary fill. Disabled dates: no contrast requirement, but ensure they're visually distinct. Verify with Contrast Checker. |
| SC 1.4.11 Non-text Contrast | AA | Today indicator, selection ring, and navigation arrows: 3:1 contrast. |
| SC 2.1.1 Keyboard | A | Full keyboard navigation through the calendar grid, month/year navigation, and date selection. |
| SC 2.4.7 Focus Visible | AA | Currently focused date cell must have a visible focus indicator. |
| SC 3.3.2 Labels or Instructions | A | Input must have a visible label. Expected format should be communicated (placeholder or helper text). |
role="dialog" with aria-label="Choose date" or aria-labelledbyrole="grid" with aria-label="[Month Year]" (e.g., "March 2026")role="gridcell". Selected: aria-selected="true". Disabled: aria-disabled="true".aria-current="date"aria-label="Previous month", aria-label="Next month"aria-live="polite" region to announce "March 2026"| Key | Action |
|---|---|
| Arrow Right | Move focus to next day |
| Arrow Left | Move focus to previous day |
| Arrow Down | Move focus to same day next week |
| Arrow Up | Move focus to same day previous week |
| Home | Move focus to first day of the current week (or month) |
| End | Move focus to last day of the current week (or month) |
| Page Down | Move to same date in next month |
| Page Up | Move to same date in previous month |
| Shift + Page Down | Move to same date in next year |
| Shift + Page Up | Move to same date in previous year |
| Enter / Space | Select the focused date |
| Escape | Close the calendar popover, return focus to input |
This keyboard model follows the WAI-ARIA grid pattern and is the expected behavior for date pickers. Users who rely on keyboard navigation depend on these exact keys.
For comprehensive patterns, see our ARIA Attributes Guide and Keyboard Accessibility Guide.
Intl.DateTimeFormat API to format dates. First day of week varies by country (Sunday in US, Monday in Europe, Saturday in Middle East).aria-disabled="true"). Show why they're disabled via tooltip if possible.<!-- Date Picker: Input + Popover Calendar -->
<div class="date-picker">
<label for="date-input" id="date-label">Departure date</label>
<div class="date-input-wrapper">
<input
type="text"
id="date-input"
placeholder="MM/DD/YYYY"
aria-describedby="date-format"
aria-haspopup="dialog"
autocomplete="off"
/>
<button
type="button"
class="calendar-toggle"
aria-label="Open calendar"
aria-expanded="false"
aria-controls="calendar-dialog"
>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M5.75 2a.75.75 0 0 1 .75.75V4h7V2.75a.75.75 0 0 1 1.5 0V4h1.25A1.75 1.75 0 0 1 18 5.75v10.5A1.75 1.75 0 0 1 16.25 18H3.75A1.75 1.75 0 0 1 2 16.25V5.75A1.75 1.75 0 0 1 3.75 4H5V2.75A.75.75 0 0 1 5.75 2ZM5 5.5H3.75a.25.25 0 0 0-.25.25V8h13V5.75a.25.25 0 0 0-.25-.25H5Zm11.5 4h-13v6.75c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V9.5Z"/>
</svg>
</button>
<span id="date-format" class="helper-text">Format: MM/DD/YYYY</span>
</div>
<!-- Calendar Dialog -->
<div
id="calendar-dialog"
role="dialog"
aria-label="Choose departure date"
aria-modal="true"
class="calendar-popover"
hidden
>
<div class="calendar-header">
<button type="button" aria-label="Previous month">◀</button>
<span aria-live="polite" class="calendar-title">March 2026</span>
<button type="button" aria-label="Next month">▶</button>
</div>
<table role="grid" aria-label="March 2026">
<thead>
<tr>
<th scope="col" abbr="Sunday">Su</th>
<th scope="col" abbr="Monday">Mo</th>
<th scope="col" abbr="Tuesday">Tu</th>
<th scope="col" abbr="Wednesday">We</th>
<th scope="col" abbr="Thursday">Th</th>
<th scope="col" abbr="Friday">Fr</th>
<th scope="col" abbr="Saturday">Sa</th>
</tr>
</thead>
<tbody>
<tr>
<td><button type="button" tabindex="-1" aria-disabled="true" class="date-cell adjacent">22</button></td>
<td><button type="button" tabindex="-1" aria-disabled="true" class="date-cell adjacent">23</button></td>
<!-- ... more cells ... -->
<td><button type="button" tabindex="0" aria-current="date" class="date-cell today">8</button></td>
<!-- ... more cells ... -->
</tr>
</tbody>
</table>
</div>
</div>import { useState, useRef, useEffect, useMemo } from "react";
interface DatePickerProps {
value?: Date | null;
onChange?: (date: Date | null) => void;
label: string;
placeholder?: string;
minDate?: Date;
maxDate?: Date;
disabledDates?: (date: Date) => boolean;
locale?: string;
error?: string;
}
export default function DatePicker({
value = null,
onChange,
label,
placeholder = "MM/DD/YYYY",
minDate,
maxDate,
disabledDates,
locale = "en-US",
error,
}: DatePickerProps) {
const [open, setOpen] = useState(false);
const [viewDate, setViewDate] = useState(value || new Date());
const [focusDate, setFocusDate] = useState(value || new Date());
const inputRef = useRef<HTMLInputElement>(null);
const calendarRef = useRef<HTMLDivElement>(null);
const daysInMonth = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 0).getDate();
const firstDay = new Date(viewDate.getFullYear(), viewDate.getMonth(), 1).getDay();
const monthLabel = viewDate.toLocaleDateString(locale, { month: "long", year: "numeric" });
const isDisabled = (date: Date) => {
if (minDate && date < minDate) return true;
if (maxDate && date > maxDate) return true;
if (disabledDates?.(date)) return true;
return false;
};
const selectDate = (day: number) => {
const date = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
if (isDisabled(date)) return;
onChange?.(date);
setOpen(false);
inputRef.current?.focus();
};
const handleKeyDown = (e: React.KeyboardEvent, day: number) => {
const current = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
let next = new Date(current);
switch (e.key) {
case "ArrowRight": next.setDate(next.getDate() + 1); break;
case "ArrowLeft": next.setDate(next.getDate() - 1); break;
case "ArrowDown": next.setDate(next.getDate() + 7); break;
case "ArrowUp": next.setDate(next.getDate() - 7); break;
case "Enter":
case " ": e.preventDefault(); selectDate(day); return;
case "Escape": setOpen(false); inputRef.current?.focus(); return;
default: return;
}
e.preventDefault();
setFocusDate(next);
if (next.getMonth() !== viewDate.getMonth()) setViewDate(next);
};
const cells = [];
for (let i = 0; i < firstDay; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
const isToday = (day: number) => {
const now = new Date();
return day === now.getDate()
&& viewDate.getMonth() === now.getMonth()
&& viewDate.getFullYear() === now.getFullYear();
};
return (
<div className="date-picker">
<label htmlFor="date-input">{label}</label>
<div className="date-input-wrapper">
<input
ref={inputRef}
id="date-input"
type="text"
placeholder={placeholder}
value={value?.toLocaleDateString(locale) ?? ""}
aria-invalid={!!error || undefined}
aria-haspopup="dialog"
readOnly
onClick={() => setOpen(!open)}
/>
</div>
{error && <span className="error-text" role="alert">{error}</span>}
{open && (
<div ref={calendarRef} role="dialog" aria-label={`Choose ${label}`} aria-modal="true" className="calendar-popover">
<div className="calendar-header">
<button type="button" aria-label="Previous month" onClick={() => setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1))}>◀</button>
<span aria-live="polite">{monthLabel}</span>
<button type="button" aria-label="Next month" onClick={() => setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1))}>▶</button>
</div>
<table role="grid" aria-label={monthLabel}>
<thead>
<tr>{["Su","Mo","Tu","We","Th","Fr","Sa"].map(d => <th key={d} scope="col">{d}</th>)}</tr>
</thead>
<tbody>
{Array.from({ length: Math.ceil(cells.length / 7) }, (_, row) => (
<tr key={row}>
{cells.slice(row * 7, row * 7 + 7).map((day, i) => (
<td key={i}>
{day && (
<button
type="button"
className={`date-cell ${isToday(day) ? "today" : ""}`}
tabIndex={focusDate.getDate() === day ? 0 : -1}
aria-selected={value?.getDate() === day && value?.getMonth() === viewDate.getMonth()}
aria-current={isToday(day) ? "date" : undefined}
aria-disabled={isDisabled(new Date(viewDate.getFullYear(), viewDate.getMonth(), day)) || undefined}
onClick={() => selectDate(day)}
onKeyDown={(e) => handleKeyDown(e, day)}
>
{day}
</button>
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}| Feature | Material 3 | Shadcn/ui | Radix | Ant Design |
|---|---|---|---|---|
| Component | DatePicker (Material UI) | Calendar + Popover (manual composition) | No date primitive | DatePicker, RangePicker |
| Calendar library | @mui/x-date-pickers (day.js/luxon/moment adapter) | react-day-picker (recommended) | N/A | dayjs internally (rc-picker) |
| Range support | DateRangePicker (MUI X Pro $) | Manual with react-day-picker | N/A | DatePicker.RangePicker |
| Keyboard | Full grid navigation | Depends on underlying lib | N/A | Full grid navigation |
| Locale support | Via date adapter | Via react-day-picker locales | N/A | Built-in, 50+ locales |
| Timezone | @mui/x-date-pickers v6+ timezone prop | Manual | N/A | dayjs timezone plugin |
| Text input | Combined input + calendar | Separate (compose yourself) | N/A | Integrated input + calendar |
| Time support | DateTimePicker | Manual time input | N/A | DatePicker with showTime |
MUI X Date Pickers is the most comprehensive date picker in the React ecosystem. The free tier handles single date and date-time. The Pro tier ($) adds date range, time range, and date-time range pickers. It uses an adapter pattern for date libraries (dayjs, luxon, date-fns, moment) so you aren't locked into one. Timezone support landed in v6 — critical for scheduling applications.
Shadcn/ui doesn't provide a date picker component directly. Instead, it documents a pattern: compose a Popover + Calendar (powered by react-day-picker) + a text Input. This composition approach gives full control but requires more assembly. The react-day-picker library itself is excellent — lightweight, accessible, and locale-aware.
Ant Design's DatePicker is batteries-included: single date, range, week picker, month picker, quarter picker, and year picker — all in one component family. It handles locale formatting, disabled dates, preset ranges ("Last 7 days"), and custom cell rendering. The trade-off is bundle size and Ant's opinionated styling.
Radix intentionally has no date picker primitive. Date pickers involve date logic (calendar math, formatting, locale), which is fundamentally different from UI behavior (focus, keyboard, ARIA). Radix focuses on UI primitives and leaves date logic to dedicated libraries.
react-day-picker (used by Shadcn and many others) is worth highlighting: it provides the calendar grid with full keyboard navigation, ARIA roles, locale support, and flexible selection modes — all headless. You bring the styling, popover behavior, and input integration.
Native <input type="date">: Browser-native date inputs have improved dramatically. They're accessible, handle locale formatting, and provide a built-in calendar. However, styling is severely limited, range selection isn't supported, and the visual design varies across browsers. For simple forms where brand consistency isn't critical, native date inputs are a solid choice.
For building accessible calendar grids from scratch, see our Keyboard Accessibility Guide.