DateRangePicker
The DateRangePicker component lets users pick a date range from a dual-month calendar, with an optional rail of consumer-supplied presets and an optional consumer-supplied footer. It is built on the same popover shell as DatePicker: the calendar and footer are slots, so their contents, staging behavior, and commit logic belong to the consumer.
Quick Start
- Installation
npm install @adaptavant/eds-core- Import
import { DateRangePicker } from '@adaptavant/eds-core';- Added
1.46.0
Default
With presets and a footer, staging is the consumer's. A preset click stages its range as the draft; drawing a custom range on the calendar does too, with the rail falling back to "Custom". Apply commits the draft via close(draft); Cancel, Escape, and outside clicks discard it via close(). The footer's Apply button is disabled until isRangeComplete is true.
function subDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() - days);
return result;
}
function startOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
}
function endOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
}
function startOfYear(date) {
return new Date(date.getFullYear(), 0, 1);
}
function endOfYear(date) {
return new Date(date.getFullYear(), 11, 31);
}
const presets = [
{
id: 'today',
label: 'Today',
getValue: ({ now }) => ({ from: now, to: now }),
},
{
id: 'last-7',
label: 'Last 7 days',
getValue: ({ now }) => ({ from: subDays(now, 6), to: now }),
},
{
id: 'last-30',
label: 'Last 30 days',
getValue: ({ now }) => ({ from: subDays(now, 29), to: now }),
},
{
id: 'this-month',
label: 'This month',
getValue: ({ now }) => ({ from: startOfMonth(now), to: endOfMonth(now) }),
},
{
id: 'this-year',
label: 'This year',
getValue: ({ now }) => ({ from: startOfYear(now), to: endOfYear(now) }),
},
];
function formatDate(date) {
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatRange(range) {
if (!range?.from || !range?.to) return undefined;
return `${formatDate(range.from)} – ${formatDate(range.to)}`;
}
function Example() {
const [applied, setApplied] = React.useState(undefined);
const [draft, setDraft] = React.useState(undefined);
return (
<Field label="Reporting period" labelVisibility="hidden">
<DateRangePicker
calendar={({ calendarProps }) => <Calendar {...calendarProps} />}
footer={({ close, isRangeComplete }) => (
<Box className="flex items-center justify-end gap-2">
<Button onClick={() => close()} variant="neutralTertiary">
Cancel
</Button>
<Button
isDisabled={!isRangeComplete}
onClick={() => close(draft)}
variant="neutralSecondary"
>
Apply
</Button>
</Box>
)}
formattedValue={formatRange(applied)}
onClose={(range) => {
if (range) {
setApplied(range);
} else {
setDraft(applied);
}
}}
onValueChange={setDraft}
placeholder="Select a range"
presets={presets}
value={draft}
/>
</Field>
);
}
return <Example />;
Note:
getValuereceives{ now }, pinned once per opening, so every preset and the active-preset match agree on what "today" means.
Without Presets
Omit presets (or pass an empty array) and the rail is not rendered at all — the popover is the calendar and footer alone. Staging and commit behavior are otherwise unchanged: draw a range on the calendar, then Apply or Cancel from the footer.
function formatDate(date) {
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatRange(range) {
if (!range?.from || !range?.to) return undefined;
return `${formatDate(range.from)} – ${formatDate(range.to)}`;
}
function Example() {
const [applied, setApplied] = React.useState(undefined);
const [draft, setDraft] = React.useState(undefined);
return (
<Field label="Reporting period" labelVisibility="hidden">
<DateRangePicker
calendar={({ calendarProps }) => <Calendar {...calendarProps} />}
footer={({ close, isRangeComplete }) => (
<Box className="flex items-center justify-end gap-2">
<Button onClick={() => close()} variant="neutralTertiary">
Cancel
</Button>
<Button
isDisabled={!isRangeComplete}
onClick={() => close(draft)}
variant="neutralSecondary"
>
Apply
</Button>
</Box>
)}
formattedValue={formatRange(applied)}
onClose={(range) => {
if (range) {
setApplied(range);
} else {
setDraft(applied);
}
}}
onValueChange={setDraft}
placeholder="Select a range"
value={draft}
/>
</Field>
);
}
return <Example />;
Without Footer
Omit footer and the popover has no controls of its own, so DateRangePicker closes itself as soon as the range has both ends — from a calendar drag or from a preset click, either way.
function subDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() - days);
return result;
}
function startOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
}
const presets = [
{
id: 'today',
label: 'Today',
getValue: ({ now }) => ({ from: now, to: now }),
},
{
id: 'last-7',
label: 'Last 7 days',
getValue: ({ now }) => ({ from: subDays(now, 6), to: now }),
},
{
id: 'this-month',
label: 'This month',
getValue: ({ now }) => ({ from: startOfMonth(now), to: now }),
},
];
function formatDate(date) {
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatRange(range) {
if (!range?.from || !range?.to) return undefined;
return `${formatDate(range.from)} – ${formatDate(range.to)}`;
}
function Example() {
const [applied, setApplied] = React.useState(undefined);
const [draft, setDraft] = React.useState(undefined);
return (
<Field label="Reporting period" labelVisibility="hidden">
<DateRangePicker
calendar={({ calendarProps }) => <Calendar {...calendarProps} />}
formattedValue={formatRange(applied)}
onClose={(range) => {
if (range) {
setApplied(range);
} else {
setDraft(applied);
}
}}
onValueChange={setDraft}
placeholder="Select a range"
presets={presets}
value={draft}
/>
</Field>
);
}
return <Example />;
Without Presets and Footer
Without either slot, the popover is the calendar alone: DateRangePicker closes itself once the drawn range is complete.
function formatDate(date) {
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatRange(range) {
if (!range?.from || !range?.to) return undefined;
return `${formatDate(range.from)} – ${formatDate(range.to)}`;
}
function Example() {
const [applied, setApplied] = React.useState(undefined);
const [draft, setDraft] = React.useState(undefined);
return (
<Field label="Reporting period" labelVisibility="hidden">
<DateRangePicker
calendar={({ calendarProps }) => <Calendar {...calendarProps} />}
formattedValue={formatRange(applied)}
onClose={(range) => {
if (range) {
setApplied(range);
} else {
setDraft(applied);
}
}}
onValueChange={setDraft}
placeholder="Select a range"
value={draft}
/>
</Field>
);
}
return <Example />;
Custom Trigger
The DateRangePicker supports a custom trigger as a render prop which allows full control over its appearance and behavior.
The following props are provided to the trigger render prop:
triggerProps: Props required for the triggerplaceholder: Placeholder text to be displayed when no range is selectedisOpen: Boolean indicating whether the date range picker is open
function formatDate(date) {
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatRange(range) {
if (!range?.from || !range?.to) return undefined;
return `${formatDate(range.from)} – ${formatDate(range.to)}`;
}
function Example() {
const [applied, setApplied] = React.useState(undefined);
const [draft, setDraft] = React.useState(undefined);
return (
<Field label="Reporting period" labelVisibility="hidden">
<DateRangePicker
calendar={({ calendarProps }) => <Calendar {...calendarProps} />}
onClose={(range) => {
if (range) {
setApplied(range);
} else {
setDraft(applied);
}
}}
onValueChange={setDraft}
placeholder="Select a range"
trigger={({ triggerProps, isOpen }) => (
<Button
{...triggerProps}
isPressed={isOpen}
variant="neutralSecondary"
>
{formatRange(applied) ?? 'Select a range'}
</Button>
)}
value={draft}
/>
</Field>
);
}
return <Example />;
API Reference
DateRangePicker
| Prop | Default | Description |
|---|---|---|
calendar | _ | (props: { calendarProps: Pick<CalendarRangeProps, 'initialFocus' | 'isDualMonthMode' | 'mode' | 'onValueChange' | 'showOutsideDays' | 'size' | 'value'>; close: (range?: DateRange) => void }) => React.ReactElementRenders the calendar inside the popover. Spread calendarProps onto Calendar, then set anything else after it. close closes the popover, optionally committing a range. |
copyLabels? | _ | objectCopy labels for accessibility and tooltips. |
copyLabels.customRangeLabel? | Custom | stringLabel for the trailing rail option, selected whenever the range matches no preset. |
copyLabels.presetsLabel? | Date range presets | stringAccessible name for the presets listbox. |
footer? | _ | (props: { close: (range?: DateRange) => void; isRangeComplete: boolean }) => React.ReactNodeRenders the footer beneath the calendar. Gate your commit control on isRangeComplete. Omit the prop and the picker closes itself once the range is complete. |
formattedValue? | _ | React.ReactNodeCustomise how the value is presented in the default trigger. |
onClose? | _ | (range?: DateRange) => voidInvoked on every close, including Escape and outside clicks. A commit carries the range, a dismissal carries nothing. |
onValueChange? | _ | (range: DateRange | undefined) => voidInvoked on every range change, from a calendar selection or a preset click alike. |
placeholder | _ | stringPlaceholder text to be displayed in the trigger. |
presets? | _ | DateRangePreset[]Presets shown in the rail beside the calendar, each one { id, label, getValue }. getValue({ now }) resolves the preset to a range. The rail is hidden when this is empty or omitted, and a "Custom" option is always appended. |
shouldUsePortal? | true | booleanDetermines whether the Popover should be rendered in a React Portal. |
trigger? | _ | (props: { triggerProps: ButtonProps; isOpen: boolean; placeholder: string }) => React.ReactElement<ButtonProps>Custom trigger component. |
value? | _ | DateRangeThe range being edited. Single source of truth: it drives the rail's active preset, the footer's isRangeComplete, and the calendar in the slot. |
Style API
Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.
Please refer to the Style API documentation for more insights.
DateRangePicker parts
function startOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
}
const presets = [
{
id: 'today',
label: 'Today',
getValue: ({ now }) => ({ from: now, to: now }),
},
{
id: 'this-month',
label: 'This month',
getValue: ({ now }) => ({ from: startOfMonth(now), to: now }),
},
];
function formatDate(date) {
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatRange(range) {
if (!range?.from || !range?.to) return undefined;
return `${formatDate(range.from)} – ${formatDate(range.to)}`;
}
function Example() {
const [applied, setApplied] = React.useState(undefined);
const [draft, setDraft] = React.useState(undefined);
return (
<Field label="Reporting period" labelVisibility="hidden">
<DateRangePicker
calendar={({ calendarProps }) => <Calendar {...calendarProps} />}
footer={({ close, isRangeComplete }) => (
<Box className="flex items-center justify-end gap-2">
<Button onClick={() => close()} variant="neutralTertiary">
Cancel
</Button>
<Button
isDisabled={!isRangeComplete}
onClick={() => close(draft)}
variant="neutralSecondary"
>
Apply
</Button>
</Box>
)}
formattedValue={formatRange(applied)}
onClose={(range) => {
if (range) {
setApplied(range);
} else {
setDraft(applied);
}
}}
onValueChange={setDraft}
placeholder="Select a range"
presets={presets}
value={draft}
classNames={{
panel: 'border-2 border-status-available',
sidebar: 'bg-subtle',
preset: 'rounded-8px',
presetLabel: 'font-mono',
}}
/>
</Field>
);
}
return <Example />;
The DateRangePicker component can be styled using the following properties:
| Stylable Parts | Description |
|---|---|
| root | The outermost wrapping element. |
| popover | The popover element that displays the panel. |
| trigger | The element that triggers the popover. |
| triggerChevron | The chevron in the trigger element. |
| triggerLabel | The label in the trigger element. |
| triggerLabelTrack | The label's track wrapper of the trigger element. |
| panel | The panel rendered inside the popover, wrapping the body and footer. |
| body | Wraps the presets rail and the calendar. |
| sidebar | The presets rail's container, rendered beside the calendar. |
| presetList | The listbox containing the preset options. |
| preset | An individual preset option, including the synthesised "Custom" option. |
| presetLabel | The label text within a preset option. |
| presetCheck | The check indicator shown on the selected preset option. |