@xsolla/xui-date-picker
v0.209.1
Published
A cross-platform date picker with an input field that opens a single- or dual-month calendar dropdown for date or range selection. <!-- BEGIN:xui-mcp-instructions:date-picker --> It’s a form field that lets users select a single date or date range. It is
Readme
DatePicker
A cross-platform date picker with an input field that opens a single- or dual-month calendar dropdown for date or range selection.
It’s a form field that lets users select a single date or date range. It is always used together with Calendar — the panel that opens on click and provides the interactive date grid. DatePicker handles the input trigger; Calendar handles the selection UI.
When to use
- When a user needs to select a specific date (booking, expiry, deadline, birth date)
- When filtering or reporting by date or date range
- When a calendar view helps the user orient themselves in time
- When you need to prevent free-text date format errors
When not to use
- When the date is derived automatically and not user-selected
Content guidelines
- Placeholder text: use "Choose a date" for single mode and "Choose a date range" for range mode — these are the defaults.
- Labels (via Field Group): keep them short and clear — "Date", "Start date", "Report period".
- Error messages: explain what went wrong — "Please select a date" or "End date must be after start date".
- Use standard labels — Today, Yesterday, Last 7 days, Last 30 days, This month, Last month. Chip labels are shown directly in the DatePicker field when a chip is selected, so keep them short and self-explanatory — the label must make sense both in the chip row and as standalone field content.
Behaviour guidelines
- When the DatePicker is clicked, the Calendar panel opens anchored below the field (or above if there is not enough space below).
- Single date mode: selecting a date closes the panel immediately and fills the field.
- Range mode: first click sets the start date; second click sets the end date and closes the panel. Hovering after the first click shows a range preview. If the second date is before the first, the range resets and the clicked date becomes the new start.
- When the user selects a chip (e.g. Last 30 days, Last 7 days, Today), the DatePicker field displays the chip label instead of the date range. This makes the selection immediately scannable — the user sees "Last 30 days" rather than "Mar 12, 2025 — Apr 11, 2025". If the user then manually adjusts the range by clicking on dates, the field switches back to showing the explicit date range.
- Pressing Escape closes the panel without changing the selection and returns focus to the field.
- The Calendar panel does not shift page layout — it renders as a floating overlay above content.
- When Range=True is set on DatePicker, always set Months=Two and Range=True on Calendar.
- When the panel opens, focus moves to the selected date cell, or to today's date if nothing is selected.
- When the panel closes, focus returns to the DatePicker field.
Accessibility
- Apply role="dialog" and aria-modal="true" to the Calendar panel container.
- Apply aria-label="Choose a date" (or range equivalent) to the dialog.
- Apply aria-selected="true" to selected date cells.
- Apply aria-disabled="true" to disabled cells and to the DatePicker field when State=Disable.
- Apply aria-current="date" to today's date cell.
- Use aria-live to announce month navigation changes to screen readers.
Installation
npm install @xsolla/xui-date-pickerImports
import {
DatePicker,
Calendar,
DualCalendar,
CalendarHeader,
CalendarGrid,
CalendarChips,
formatDate,
} from '@xsolla/xui-date-picker';
import type {
DatePickerProps,
DatePickerPlacement,
DateRangeType,
CalendarProps,
DualCalendarProps,
CalendarChipOption,
CalendarChipsProps,
CalendarGridProps,
CalendarHeaderProps,
CalendarLocaleType,
} from '@xsolla/xui-date-picker';Calendar, DualCalendar and friends are re-exported from @xsolla/xui-calendar. For standalone calendar usage, depend on @xsolla/xui-calendar directly.
Quick start
const [date, setDate] = useState<Date | null>(null);
<DatePicker
selectedDate={date}
onChange={(d) => setDate(d as Date | null)}
placeholder="Select a date"
/>;API Reference
<DatePicker>
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| variant | 'single' \| 'dual' | 'single' | Single-month or dual-month calendar; 'dual' forces range mode. |
| selectedDate | Date \| null | — | Selected date for single mode. |
| startDate | Date \| null | — | Range start date. |
| endDate | Date \| null | — | Range end date. |
| selectsRange | boolean | false | Enable range selection. |
| onChange | (date: Date \| DateRangeType) => void | — | Fired when the date or range changes. |
| placeholder | string | 'Choose a date' / 'Choose a date range' | Placeholder for the input, shown while it is empty. Not derived from dateFormat. |
| dateFormat | string | 'MM/dd/yyyy' | date-fns format used to render and parse the field. Numeric tokens only — see Date formats. |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Input size. |
| disabled | boolean | false | Disable the input. |
| backgroundColor | string | — | Custom background colour for the input. |
| dropdownPosition | 'left' \| 'right' | 'left' | Deprecated — use the alignment suffix on placement (dropdownPosition="right" → placement="bottom-end"). Still honoured as a fallback when placement has no suffix. |
| placement | DatePickerPlacement | auto | Where the calendar opens, as {side}[-alignment] — same {side}-{start\|center\|end} convention as ContextMenuPlacement. A bare side aligns to start. When omitted, the calendar measures viewport space on open and auto-flips, preferring bottom → top → left → right, also flipping the cross-axis edge to stay on screen. When set, it always opens at that placement. Web only. |
| closeOnComplete | boolean | true | Whether picking a date in the calendar dismisses the panel. false keeps it open after a complete selection so a bottomContent footer stays reachable — see Footer actions. |
| locale | CalendarLocaleType | 'enUS' | date-fns locale identifier. |
| firstDayOfWeek | number | — | First day of the week (0 = Sunday … 6 = Saturday). |
| initialMonth | Date | — | Month shown on first render. |
| month | Date | — | Controlled month. |
| minDate | Date \| null | — | Minimum selectable date. |
| maxDate | Date \| null | — | Maximum selectable date. |
| chips | CalendarChipOption[] | — | Preset range chips above the calendar. |
| activeChip | string \| null | — | Currently active chip value. |
| onChipSelect | (value: string \| null) => void | — | Fired when a chip is selected. |
| topContent | (api: { close: () => void }) => ReactNode | — | Custom content above the calendar. close() dismisses the picker panel. |
| bottomContent | (api: { close: () => void }) => ReactNode | — | Custom content below the calendar. close() dismisses the picker panel — pair it with closeOnComplete={false} for an Apply/Cancel footer. |
| contextMenuMaxHeight | number | — | Max height for the month/year context menus. |
| testID | string | — | Test identifier. |
| overlayThemeMode | ThemeMode | themeMode | Theme mode for the calendar overlay. |
| overlayThemeProductContext | ProductContext | themeProductContext | Product context for the calendar overlay. |
DatePicker extends CalendarProps (excluding onChange).
Inherits ThemeOverrideProps (themeMode, themeProductContext).
Date formats
The field is edited segment by segment (type digits, arrow-step a segment, tab between them), so dateFormat supports numeric tokens only — exactly three, in any order, with anything between them treated as a literal separator:
| Token | Segment | Rendered as |
| --- | --- | --- |
| d, dd | Day | Always two digits (05) |
| M, MM | Month | Always two digits (03) |
| y, yyyy | Year | Always four digits (2026) |
<DatePicker dateFormat="dd/MM/yyyy" /> // 15/03/2026
<DatePicker dateFormat="yyyy-MM-dd" /> // 2026-03-15
<DatePicker dateFormat="dd.MM.yyyy" /> // 15.03.2026While the field is focused or partly filled it shows a mask built from the format (dd/mm/yyyy), which is separate from placeholder — the placeholder shows only while the field is empty and does not track dateFormat.
Mind the casing. date-fns tokens are not Moment.js/Day.js tokens: month is MM (mm is minutes), day is dd (DD is day-of-year) and year is yyyy (YYYY is week-numbering year). Passing dd/mm/yyyy, DD/MM/YYYY, dd/MM/YYYY or DD.MM.YYYY is read as the intended dd/MM/yyyy and logs a console.warn in development naming the token and its replacement. Fix the format string rather than relying on the repair.
Not supported: textual and localised tokens (MMM, MMMM, P, PP), time tokens (HH:mm), escaped literals ('de'), and formats missing one of the three segments (MM/yyyy — the field could never be completed, so onChange would never fire). These warn in development and fall back to MM/dd/yyyy, so the field stays completable and never renders raw token text as though it were a value. To display a date as Mar 15, 2026, format it yourself with formatDate outside the picker.
<Calendar>
Re-exported from @xsolla/xui-calendar. Standalone single-month calendar.
<DualCalendar>
Re-exported from @xsolla/xui-calendar. Two-month calendar for range selection.
<CalendarHeader>
Re-exported from @xsolla/xui-calendar. Month/year header with navigation controls.
<CalendarGrid>
Re-exported from @xsolla/xui-calendar. Day grid; takes currentMonth and selection props.
<CalendarChips>
Re-exported from @xsolla/xui-calendar. Preset chips bar (e.g. "Last 7 days", "This month").
Utilities
| Export | Type | Description |
| --- | --- | --- |
| formatDate | (date: Date, formatStr: string, locale?: CalendarLocaleType) => string | Format a date with a date-fns locale identifier. Re-exported from @xsolla/xui-calendar; the full date-fns token set applies, so use it for display text the picker's own field cannot render (MMM d, yyyy). |
Types
type DateRangeType = [Date | null, Date | null];
type DatePickerSide = 'top' | 'bottom' | 'left' | 'right';
type DatePickerAlignment = 'start' | 'center' | 'end';
type DatePickerPlacement = DatePickerSide | `${DatePickerSide}-${DatePickerAlignment}`;Examples
Range selection
const [start, setStart] = useState<Date | null>(null);
const [end, setEnd] = useState<Date | null>(null);
<DatePicker
selectsRange
startDate={start}
endDate={end}
onChange={(range) => {
const [s, e] = range as DateRangeType;
setStart(s);
setEnd(e);
}}
/>;Dual calendar
const [start, setStart] = useState<Date | null>(null);
const [end, setEnd] = useState<Date | null>(null);
<DatePicker
variant="dual"
placement="bottom-end"
startDate={start}
endDate={end}
onChange={(range) => {
const [s, e] = range as DateRangeType;
setStart(s);
setEnd(e);
}}
/>;Footer actions
By default the panel dismisses itself as soon as the selection is complete: one click in single mode, the second day click in range mode. That default is wrong for a picker with an interactive footer — the second click tears the panel down before an Apply button in bottomContent can be pressed.
closeOnComplete={false} keeps the panel open after a complete selection and hands dismissal to the footer. The picker calls bottomContent/topContent with a close() that actually closes it, so Apply/Cancel behave as written. Escape and an outside click still close the panel either way, and typed or pasted entry never closes it regardless of the prop.
const [draft, setDraft] = useState<DateRangeType>([null, null]);
const [applied, setApplied] = useState<DateRangeType>([null, null]);
<DatePicker
variant="dual"
closeOnComplete={false}
startDate={draft[0]}
endDate={draft[1]}
onChange={(range) => setDraft(range as DateRangeType)}
bottomContent={({ close }) => (
<ButtonGroup split>
<Button
size="sm"
variant="secondary"
tone="mono"
onPress={() => {
setDraft(applied);
close();
}}
>
Cancel
</Button>
<Button
size="sm"
disabled={!draft[0] || !draft[1]}
onPress={() => {
setApplied(draft);
close();
}}
>
Apply
</Button>
</ButtonGroup>
)}
/>;Placement and autoflip
const [date, setDate] = useState<Date | null>(null);
// Default: the calendar auto-flips based on available viewport space.
<DatePicker selectedDate={date} onChange={(d) => setDate(d as Date | null)} />;
// Force a side with `placement`.
<DatePicker
placement="top"
selectedDate={date}
onChange={(d) => setDate(d as Date | null)}
/>;
// Force a side and alignment (here: opens upward, right-aligned).
<DatePicker
placement="top-end"
selectedDate={date}
onChange={(d) => setDate(d as Date | null)}
/>;Preset chips
const chips: CalendarChipOption[] = [
{ label: 'Today', value: 'today', days: 0 },
{ label: 'Last 7 days', value: 'last7', days: 7 },
{ label: 'Last 30 days', value: 'last30', days: 30 },
];
const [activeChip, setActiveChip] = useState<string | null>(null);
const [date, setDate] = useState<Date | null>(null);
<DatePicker
chips={chips}
activeChip={activeChip}
onChipSelect={setActiveChip}
selectedDate={date}
onChange={(d) => setDate(d as Date | null)}
/>;Sizes
<DatePicker size="xs" placeholder="Extra small" />
<DatePicker size="sm" placeholder="Small" />
<DatePicker size="md" placeholder="Medium" />
<DatePicker size="lg" placeholder="Large" />
<DatePicker size="xl" placeholder="Extra large" />Min/max date
const [date, setDate] = useState<Date | null>(null);
<DatePicker
selectedDate={date}
onChange={(d) => setDate(d as Date | null)}
minDate={new Date(2024, 0, 1)}
maxDate={new Date(2024, 11, 31)}
/>;Accessibility
- The input is keyboard accessible; the calendar opens on focus.
- Calendar dates are navigable via keyboard; selected dates are announced to screen readers.
- Focus is restored to the input when the calendar closes.
- Dates outside
minDate/maxDateare disabled.
