Skip to content

Date Picker

Allows users to select a date or date range through a calendar interface.

  • Calendar
  • Date Input

Overview

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:

  • Selecting a specific calendar date (birth date, appointment, deadline)
  • Selecting a date range (hotel check-in/check-out, reporting period)
  • When the user needs to see surrounding dates for context (availability, pricing)
  • When relative date context matters (is it a weekday? which week of the month?)

When NOT to use a Date Picker:

  • For known dates the user can type easily (birth date) — a simple Text Input with date masking (MM/DD/YYYY) may be faster. Offer both input + calendar.
  • For relative dates ("last 7 days", "this quarter") — use a Select with predefined options.
  • For time-only input — use a time picker (separate component or input with type="time").
  • For far-future dates (year 2040) — navigating month-by-month is tedious. Add year/month dropdowns or a text input fallback.

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.

Variants

Functional Variants

VariantDescriptionUse Case
Single dateSelect one specific dateBirth date, deadline, appointment
Date rangeSelect a start and end dateHotel booking, reporting period, leave request
Multi-dateSelect multiple non-contiguous datesScheduling recurring events, marking exceptions
Date-timeDate + time picker combinedMeeting scheduler, event creation, timestamps
Month pickerSelect a month and year (no day)Credit card expiry, monthly reports
Year pickerSelect a year onlyHistorical data filtering, birth year

Display Variants

VariantDescription
Input + PopoverA text input with a calendar icon that opens a popover calendar. Default and most common.
Inline calendarCalendar is always visible, embedded in the page. No popover. For booking/scheduling UIs.
Dual calendarTwo months displayed side-by-side. Essential for date range selection.
Input-onlyText input with date masking, no calendar. For known dates (birth date).

Size Variants

SizeInput HeightCalendar WidthUse Case
Small32px280pxDense forms, inline filters
Medium40px320pxDefault forms
Large48px360pxMobile-first, booking interfaces

Properties

Date Picker Properties

PropertyTypeDefaultDescription
valueDate | nullnullSelected date (controlled)
onChange(date: Date | null) => void—Callback when date changes
defaultValueDate—Initial date (uncontrolled)
minDateDate—Earliest selectable date
maxDateDate—Latest selectable date
disabledDates(date: Date) => boolean—Function to disable specific dates (holidays, unavailable days)
localestringBrowser localeBCP 47 locale string ('en-US', 'de-DE', 'ja-JP')
firstDayOfWeek0-6Locale default0 = Sunday, 1 = Monday
formatstringLocale defaultDisplay format ('MM/dd/yyyy', 'dd.MM.yyyy')
placeholderstringFormat stringInput placeholder text
requiredbooleanfalseForm validation required
disabledbooleanfalseDisables the entire picker
readOnlybooleanfalseAllows viewing the calendar but not changing the value
errorstring—Error message
clearablebooleanfalseShows a clear button to reset the value

Date Range Properties (additional)

PropertyTypeDefaultDescription
startDateDate | nullnullRange start
endDateDate | nullnullRange end
onRangeChange(range: { start: Date; end: Date }) => void—Callback for range changes
minLengthnumber—Minimum range length in days
maxLengthnumber—Maximum range length in days

Token Mappings

Design Token Mappings

Token CategoryToken ExampleDate Picker Usage
Color – Input--color-surface, --color-borderInput field background and border
Color – Calendar Surface--color-surface-elevatedCalendar popover background
Color – Selected--color-primary-600Selected date circle fill. Choose accessible hues with Color Palette Generator.
Color – Selected Text--color-on-primaryText on the selected date
Color – Range--color-primary-100Background 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-disabledDates outside the selectable range
Color – Hover--color-surface-hoverDate cell hover background
Color – Adjacent Month--color-text-tertiaryDates from previous/next month (dimmed)
Border Radius--radius-full (9999px)Selected date circle
Border Radius – Calendar--radius-xl (16px)Calendar popover corners
Shadow--shadow-lgCalendar popover elevation
Spacing--space-1 (4px)Gap between date cells
Typography--font-size-sm, --font-variant-numeric: tabular-numsDate numbers (tabular for alignment)

See our Design Tokens Complete Guide for naming conventions.

States

Input States

StateVisual Treatment
EmptyPlaceholder showing the expected format (e.g., "MM/DD/YYYY")
FilledFormatted date displayed in the input
FocusedInput border highlighted. Calendar popover opens (optional — some implementations open on icon click only).
ErrorRed border, error message below. Invalid date entered or required field empty.
DisabledGreyed out input and calendar icon. Not interactive.

Calendar States

StateVisual Treatment
IdleCurrent month displayed. Today highlighted with subtle indicator (dot or ring).
Date hoverBackground highlight on hovered date cell
Date selectedSolid circle (primary color) on the selected date. White text.
Range: start/endSolid circles on start and end dates
Range: betweenLight primary background connecting start and end dates
Range: previewDuring selection (start chosen, hovering for end), show a preview of the range with lighter styling
Disabled dateMuted text, no hover effect, not selectable. Use aria-disabled="true".
Adjacent monthDates from previous/next month shown in muted text at the edges of the grid
Month/Year navigationPrev/Next arrows (◀ ▶) for month navigation. Month/year header may be clickable for month/year picker views.

Calendar Navigation States

ViewDescription
Day viewDefault. 6×7 grid of dates for one month.
Month view3×4 grid of months. User picks a month, returns to day view.
Year viewGrid of years (e.g., 2020–2035). User picks a year, goes to month view, then day view.
Decade viewRange of decades for far-past/future date selection.

Accessibility

WCAG Requirements

CriterionLevelRequirement
SC 1.3.1 Info and RelationshipsACalendar 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)AADate 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 ContrastAAToday indicator, selection ring, and navigation arrows: 3:1 contrast.
SC 2.1.1 KeyboardAFull keyboard navigation through the calendar grid, month/year navigation, and date selection.
SC 2.4.7 Focus VisibleAACurrently focused date cell must have a visible focus indicator.
SC 3.3.2 Labels or InstructionsAInput must have a visible label. Expected format should be communicated (placeholder or helper text).

ARIA Attributes

  • Calendar container: role="dialog" with aria-label="Choose date" or aria-labelledby
  • Grid: role="grid" with aria-label="[Month Year]" (e.g., "March 2026")
  • Day cells: role="gridcell". Selected: aria-selected="true". Disabled: aria-disabled="true".
  • Today: Mark with aria-current="date"
  • Navigation buttons: aria-label="Previous month", aria-label="Next month"
  • Live announcements: When the month changes, use an aria-live="polite" region to announce "March 2026"

Keyboard Interaction

KeyAction
Arrow RightMove focus to next day
Arrow LeftMove focus to previous day
Arrow DownMove focus to same day next week
Arrow UpMove focus to same day previous week
HomeMove focus to first day of the current week (or month)
EndMove focus to last day of the current week (or month)
Page DownMove to same date in next month
Page UpMove to same date in previous month
Shift + Page DownMove to same date in next year
Shift + Page UpMove to same date in previous year
Enter / SpaceSelect the focused date
EscapeClose 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.

Common Accessibility Mistakes

  1. No keyboard navigation in the calendar grid. This is the most common failure — many date pickers are mouse-only.
  2. Missing live region announcements when navigating between months. Without them, screen reader users don't know the month changed.
  3. Relying on color alone to indicate "selected" or "today." Add a shape change (filled circle vs. outline) in addition to color.
  4. No text input fallback. Some users (especially screen reader users) strongly prefer typing a date directly. Always offer a typeable input alongside the visual calendar.

For comprehensive patterns, see our ARIA Attributes Guide and Keyboard Accessibility Guide.

Usage Guidelines

Do's

  • ✅ Always provide a text input alongside the calendar. Power users type dates faster than they click them. Screen reader users may prefer text input entirely.
  • ✅ Show the expected date format in the placeholder or helper text. "MM/DD/YYYY" isn't universal — Europeans expect "DD/MM/YYYY" or "DD.MM.YYYY".
  • ✅ Respect the user's locale. Use the Intl.DateTimeFormat API to format dates. First day of week varies by country (Sunday in US, Monday in Europe, Saturday in Middle East).
  • ✅ Pre-navigate to a sensible month. If picking a future date, start on the current month. If editing an existing date, start on that date's month.
  • ✅ Use dual calendars for range selection. Seeing two months side-by-side makes range selection intuitive. Single calendars require tedious month-switching.
  • ✅ Disable unavailable dates visually AND semantically (aria-disabled="true"). Show why they're disabled via tooltip if possible.

Don'ts

  • ❌ Don't use a date picker for birth dates in most contexts. A text input with validation is faster — users know their birthday and don't want to click back 30 years in a calendar. If you must use a calendar, provide year/month dropdowns.
  • ❌ Don't auto-close the calendar on date selection for range pickers. The user still needs to pick the end date.
  • ❌ Don't use ambiguous formats without labeling. Is "03/04/2026" March 4th or April 3rd? Always label the format or use unambiguous display ("Mar 4, 2026").
  • ❌ Don't allow invalid date entry without feedback. If the user types "02/31/2026", show an immediate error — don't silently correct it or fail on submit.
  • ❌ Don't forget timezone handling. If the date affects scheduling, clarify the timezone. A date picker alone doesn't communicate timezone — add it in helper text or a separate selector.

Internationalization Checklist

  • Date format follows locale conventions
  • First day of week is locale-appropriate
  • Month and day names are translated
  • Calendar direction matches text direction (RTL support)
  • Non-Gregorian calendars supported where needed (Islamic, Hebrew, Japanese)
  • Date parsing handles multiple input formats gracefully

Code Snippets

html
<!-- 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>
tsx
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>
  );
}

Design Systems

Cross-System Comparison

FeatureMaterial 3Shadcn/uiRadixAnt Design
ComponentDatePicker (Material UI)Calendar + Popover (manual composition)No date primitiveDatePicker, RangePicker
Calendar library@mui/x-date-pickers (day.js/luxon/moment adapter)react-day-picker (recommended)N/Adayjs internally (rc-picker)
Range supportDateRangePicker (MUI X Pro $)Manual with react-day-pickerN/ADatePicker.RangePicker
KeyboardFull grid navigationDepends on underlying libN/AFull grid navigation
Locale supportVia date adapterVia react-day-picker localesN/ABuilt-in, 50+ locales
Timezone@mui/x-date-pickers v6+ timezone propManualN/Adayjs timezone plugin
Text inputCombined input + calendarSeparate (compose yourself)N/AIntegrated input + calendar
Time supportDateTimePickerManual time inputN/ADatePicker with showTime

Notable Approaches

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.

Data InputCalendarDate Input