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.

, Select a range
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: getValue receives { 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.

, Select a range
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.

, Select a range
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.

, Select a range
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 trigger
  • placeholder: Placeholder text to be displayed when no range is selected
  • isOpen: 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

PropDefaultDescription
calendar_(props: { calendarProps: Pick<CalendarRangeProps, 'initialFocus' | 'isDualMonthMode' | 'mode' | 'onValueChange' | 'showOutsideDays' | 'size' | 'value'>; close: (range?: DateRange) => void }) => React.ReactElement
Renders the calendar inside the popover. Spread calendarProps onto Calendar, then set anything else after it. close closes the popover, optionally committing a range.
copyLabels?_object
Copy labels for accessibility and tooltips.
copyLabels.customRangeLabel?Customstring
Label for the trailing rail option, selected whenever the range matches no preset.
copyLabels.presetsLabel?Date range presetsstring
Accessible name for the presets listbox.
footer?_(props: { close: (range?: DateRange) => void; isRangeComplete: boolean }) => React.ReactNode
Renders 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.ReactNode
Customise how the value is presented in the default trigger.
onClose?_(range?: DateRange) => void
Invoked on every close, including Escape and outside clicks. A commit carries the range, a dismissal carries nothing.
onValueChange?_(range: DateRange | undefined) => void
Invoked on every range change, from a calendar selection or a preset click alike.
placeholder_string
Placeholder 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?trueboolean
Determines 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?_DateRange
The 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​

, Select a range
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 PartsDescription
rootThe outermost wrapping element.
popoverThe popover element that displays the panel.
triggerThe element that triggers the popover.
triggerChevronThe chevron in the trigger element.
triggerLabelThe label in the trigger element.
triggerLabelTrackThe label's track wrapper of the trigger element.
panelThe panel rendered inside the popover, wrapping the body and footer.
bodyWraps the presets rail and the calendar.
sidebarThe presets rail's container, rendered beside the calendar.
presetListThe listbox containing the preset options.
presetAn individual preset option, including the synthesised "Custom" option.
presetLabelThe label text within a preset option.
presetCheckThe check indicator shown on the selected preset option.