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
Modal Date Picker
A dialog that floats over the app content with a scrim overlay. Most common pattern for mobile-first UIs.
Docked Date Picker
Appears inline, anchored below a trigger element (e.g., a text field). Suitable for desktop and compact forms.
Date Range Picker
Allows selecting a start and end date. Range is highlighted with SecondaryContainer color between endpoints.
Input Mode
Combines a text field with the calendar. Users can toggle between calendar UI and manual text entry using the keyboard icon.
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
confirmButtonanddismissButtonso users can cancel without committing - Use
useDatePickerStateoutside the dialog component to persist state between opens - Pass
localeto match the user's system language - Use
selectableDatesto enforce business rules (no past dates, no weekends)
Don't
- Don't use
DatePickerwithoutDatePickerDialogfor 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
| Token | CSS Variable | Fallback |
|---|---|---|
| Container background | var(--md-sys-color-surface-container-high) | #ece6f0 |
| Selected day | var(--md-sys-color-primary) | #6750a4 |
| Selected day text | var(--md-sys-color-on-primary) | #ffffff |
| Today outline | var(--md-sys-color-primary) | #6750a4 |
| Range highlight | var(--md-sys-color-secondary-container) | #e8def8 |
| Range text | var(--md-sys-color-on-secondary-container) | #1d192b |
Shape
| Token | CSS Variable | Value |
|---|---|---|
| Dialog container | var(--md-sys-shape-corner-extra-large) | 28px |
| Day cell | var(--md-sys-shape-corner-full) | 9999px |
Dimensions (fixed)
| Token | Value |
|---|---|
| Dialog width | 360px |
| Dialog height | 568px |
| Day cell | 40×40px |
| Header height | 120px |
Accessibility
- Keyboard Navigation: Arrow keys navigate days,
Enter/Spaceselects,Escapedismisses dialog - ARIA Roles: Calendar uses
role="grid", days userole="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-motionvia Framer Motion defaults
API Reference
useDatePickerState(options?)
| Option | Type | Default | Description |
|---|---|---|---|
initialSelectedDateMs | number | null | null | Pre-selected date (UTC ms) |
initialDisplayedMonthMs | number | current month | Initial calendar month |
yearRange | [number, number] | [1900, 2100] | Selectable year range |
selectableDates | SelectableDates | all allowed | Custom date/year filter |
initialDisplayMode | 'picker' | 'input' | 'picker' | Initial UI mode |
locale | DateLocale | { weekStartsOn: 1 } | Locale + week config |
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
state | DatePickerState | required | State from useDatePickerState() |
showModeToggle | boolean | true | Show calendar/keyboard toggle icon |
title | ReactNode | "Select date" | Header title slot |
headline | ReactNode | formatted date | Header headline slot |
className | string | — | Additional CSS classes |
DatePickerDialog
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | required | Open/close state |
onDismiss | () => void | required | Called on close (scrim, Escape) |
confirmButton | ReactNode | required | Confirm action button |
dismissButton | ReactNode | — | Cancel action button |
children | ReactNode | required | DatePicker or DateRangePicker |
className | string | — | Additional CSS classes |
useDateRangePickerState(options?)
| Option | Type | Default | Description |
|---|---|---|---|
initialStartMs | number | null | null | Initial start date |
initialEndMs | number | null | null | Initial end date |
yearRange | [number, number] | [1900, 2100] | Selectable year range |
selectableDates | SelectableDates | all allowed | Date/year filter |
locale | DateLocale | { weekStartsOn: 1 } | Locale config |
DateRangePicker
| Prop | Type | Default | Description |
|---|---|---|---|
state | DateRangePickerState | required | State from useDateRangePickerState() |
showModeToggle | boolean | false | Show mode toggle |
title | ReactNode | "Select dates" | Header title slot |
className | string | — | Additional CSS classes |