MD3
Expressive
MATERIAL DESIGN 3 EXPRESSIVE

Date Pickers

Date pickers let users select a date, a date range, or manually enter a date. They use a calendar dialog or text input for flexible date selection.

Date pickers provide a visual calendar interface for date selection, with support for both modal and docked layouts. Built on MD3 Expressive motion principles with spring-based animations.

Introduction

The MD3 Expressive Date Picker system is built on a hoisted state architecture — state is managed externally via hooks (useDatePickerState, useDateRangePickerState) and passed down to display components. This mirrors the Android Compose Material3 pattern and enables full control over the picker's behavior from the parent.

The system supports:

  • Locale-aware formatting via Intl.DateTimeFormat (zero bundle overhead)
  • Configurable week start — Monday (default, ISO 8601) or Sunday
  • SelectableDates — filter any day or year from being selectable
  • MD3 Expressive spring animations — dialog enter/exit, month slide, year grid

Anatomy

  • Header: Displays title ("Select date"), selected date headline, and the month/year navigation row
  • Calendar Grid: 6×7 grid of day cells, with slide animation on month navigate
  • Month and Year Selectors: Separate, scrollable month and year lists toggled from the header
  • Action Buttons: Confirm and Cancel buttons (in modal mode)
  • Input Mode: Text field input for manual date entry (MM/DD/YYYY)

Variants

A dialog that floats over the app content with a scrim overlay. Most common pattern for mobile-first UIs.

Loading demo...

Docked Date Picker

Appears inline, anchored below a trigger element (e.g., a text field). Suitable for desktop and compact forms.

Loading demo...

Date Range Picker

Allows selecting a start and end date. Range is highlighted with SecondaryContainer color between endpoints.

Loading demo...

Input Mode

Combines a text field with the calendar. Users can toggle between calendar UI and manual text entry using the keyboard icon.

Loading demo...

Features

Expressive Motion

  • Dialog enter: DEFAULT_SPATIAL_SPRING (stiffness 380, dampingRatio 0.8) — scale + fade
  • Month navigation: FAST_SPATIAL_SPRING (stiffness 800) — direction-aware slide
  • Month/year selector: DEFAULT_EFFECTS_SPRING — fade in/out

Month and Year Selection

Use the independent month and year controls in the calendar header to jump directly to a month or year. Each control opens a scrollable list, marks the currently displayed value, and returns to the calendar after selection. Navigation controls and selector values respect both yearRange and selectableDates.isSelectableYear.

Internationalization (i18n)

All date formatting uses the native Intl.DateTimeFormat API. Pass locale to control the language and week start day:

const state = useDatePickerState({
  locale: { locale: 'vi-VN', weekStartsOn: 1 },
});

SelectableDates

Filter any date or year from being selectable:

const state = useDatePickerState({
  selectableDates: {
    // Block weekends
    isSelectableDate: (utcMs) => {
      const day = new Date(utcMs).getUTCDay();
      return day !== 0 && day !== 6;
    },
    // Only allow 2020 and after
    isSelectableYear: (year) => year >= 2020,
  },
});

Usage

Basic Modal Date Picker

import {
  DatePicker,
  DatePickerDialog,
  useDatePickerState,
  Button,
} from "@bug-on/m3-expressive";

function MyDatePicker() {
  const [open, setOpen] = React.useState(false);
  const state = useDatePickerState({
    locale: { locale: "en-US", weekStartsOn: 1 },
  });

  return (
    <>
      <Button onClick={() => setOpen(true)}>Pick a Date</Button>

      <DatePickerDialog
        open={open}
        onDismiss={() => setOpen(false)}
        confirmButton={
          <Button variant="text" onClick={() => setOpen(false)}>OK</Button>
        }
        dismissButton={
          <Button variant="text" onClick={() => setOpen(false)}>Cancel</Button>
        }
      >
        <DatePicker state={state} />
      </DatePickerDialog>
    </>
  );
}

Date Range Picker

import {
  DateRangePicker,
  DatePickerDialog,
  useDateRangePickerState,
} from "@bug-on/m3-expressive";

function MyRangePicker() {
  const [open, setOpen] = React.useState(false);
  const state = useDateRangePickerState();

  return (
    <DatePickerDialog
      open={open}
      onDismiss={() => setOpen(false)}
      confirmButton={<Button variant="text" onClick={() => setOpen(false)}>OK</Button>}
    >
      <DateRangePicker state={state} title="Select travel dates" />
    </DatePickerDialog>
  );
}

Reading Selected Date

// Single date
const dateMs = state.selectedDateMs; // number | null (UTC ms)

// Range
const startMs = rangeState.selectedStartMs;
const endMs = rangeState.selectedEndMs;

// Format for display
if (dateMs !== null) {
  const label = new Intl.DateTimeFormat('en-US', {
    year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
  }).format(new Date(dateMs));
}

Best Practices

Do

  • Always provide confirmButton and dismissButton so users can cancel without committing
  • Use useDatePickerState outside the dialog component to persist state between opens
  • Pass locale to match the user's system language
  • Use selectableDates to enforce business rules (no past dates, no weekends)

Don't

  • Don't use DatePicker without DatePickerDialog for modal UX — it will render inline
  • Don't commit the selected date until the user presses "OK"
  • Don't override spring animations with CSS transitions — they break the MD3 feel

Design Tokens

Colors

TokenCSS VariableFallback
Container backgroundvar(--md-sys-color-surface-container-high)#ece6f0
Selected dayvar(--md-sys-color-primary)#6750a4
Selected day textvar(--md-sys-color-on-primary)#ffffff
Today outlinevar(--md-sys-color-primary)#6750a4
Range highlightvar(--md-sys-color-secondary-container)#e8def8
Range textvar(--md-sys-color-on-secondary-container)#1d192b

Shape

TokenCSS VariableValue
Dialog containervar(--md-sys-shape-corner-extra-large)28px
Day cellvar(--md-sys-shape-corner-full)9999px

Dimensions (fixed)

TokenValue
Dialog width360px
Dialog height568px
Day cell40×40px
Header height120px

Accessibility

  • Keyboard Navigation: Arrow keys navigate days, Enter/Space selects, Escape dismisses dialog
  • ARIA Roles: Calendar uses role="grid", days use role="gridcell", aria-selected, aria-disabled
  • Screen Reader Labels: Each day cell has a full aria-label (e.g., "June 26, 2025")
  • Focus Management: Focus is trapped within the dialog when open
  • Reduced Motion: Animations respect prefers-reduced-motion via Framer Motion defaults

API Reference

useDatePickerState(options?)

OptionTypeDefaultDescription
initialSelectedDateMsnumber | nullnullPre-selected date (UTC ms)
initialDisplayedMonthMsnumbercurrent monthInitial calendar month
yearRange[number, number][1900, 2100]Selectable year range
selectableDatesSelectableDatesall allowedCustom date/year filter
initialDisplayMode'picker' | 'input''picker'Initial UI mode
localeDateLocale{ weekStartsOn: 1 }Locale + week config

DatePicker

PropTypeDefaultDescription
stateDatePickerStaterequiredState from useDatePickerState()
showModeTogglebooleantrueShow calendar/keyboard toggle icon
titleReactNode"Select date"Header title slot
headlineReactNodeformatted dateHeader headline slot
classNamestringAdditional CSS classes

DatePickerDialog

PropTypeDefaultDescription
openbooleanrequiredOpen/close state
onDismiss() => voidrequiredCalled on close (scrim, Escape)
confirmButtonReactNoderequiredConfirm action button
dismissButtonReactNodeCancel action button
childrenReactNoderequiredDatePicker or DateRangePicker
classNamestringAdditional CSS classes

useDateRangePickerState(options?)

OptionTypeDefaultDescription
initialStartMsnumber | nullnullInitial start date
initialEndMsnumber | nullnullInitial end date
yearRange[number, number][1900, 2100]Selectable year range
selectableDatesSelectableDatesall allowedDate/year filter
localeDateLocale{ weekStartsOn: 1 }Locale config

DateRangePicker

PropTypeDefaultDescription
stateDateRangePickerStaterequiredState from useDateRangePickerState()
showModeTogglebooleanfalseShow mode toggle
titleReactNode"Select dates"Header title slot
classNamestringAdditional CSS classes