Source: https://monarchui.dev/components/date-range-picker/

Components · Search and discovery

# DateRangePicker

Start and end dates: typed or picked on a one- or two-month calendar with a live preview, presets and length limits.

@rookcom/monarch-react/scheduling

Component

## Report period with presets

## Usage examples

### Leave with a minimum length

### Compact filter

## Purpose

Picks a period such as a report range, a sprint, leave or a filter on created and due dates. Both ends can be typed in the locale’s format or picked on a calendar that previews the range as the pointer or focus moves; presets cover common periods. The value is `{ start, end }`, inclusive ISO dates. Import from `@rookcom/monarch-react/scheduling`.

### Use when

- Report and analytics periods with presets (Last 7 days, Last month).
- Filtering lists by a created, due or updated date range.
- Booking a span of days (leave, sprints, campaigns) with a minimum or maximum length.

### Avoid when

- Only a few fixed periods and no custom range: use SegmentedControl.
- Two unrelated dates (a start date and a separate review date): use two DatePickers.

## Design principles

- One control for one period: a single box with two inputs, one calendar, one error.
- The range is only committed when it is complete and valid, so filters never run on half a range.
- The calendar previews the range before the second click, and blocks days that would break the length limits.
- Two months where there is room, one on phones.

## API reference

```tsx
import { DateRangePicker } from '@rookcom/monarch-react/scheduling';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `DateRangePicker` | component | client | `.mn-date-range-picker` |

The ref goes to the root `role="group"` element.

`startName` and `endName` add hidden inputs for form submission.

Generated from the TypeScript declarations of `packages/react/src/components/search/date-range-picker`.

## Properties

`value`/`onValueChange` (controlled) or `defaultValue`; `onValueChange` receives a complete `{ start, end }` or `null`.

Presets: `{ lastDays }` (ending today), `{ nextDays }` (starting today), `{ months }` (0 = this calendar month, -1 = last), or a fixed `{ range }`. Presets are clipped to `min` and `max` (with `max` today, “This month” is month to date) and disabled when what is left breaks the limits.

`minDays`/`maxDays` count both ends: Monday to Sunday is 7 days.

`onValidationChange` reports a date error, `end-before-start`, `too-short`, `too-long` or `null`.

### `DateRangePicker`

Two dates that bound a period: typed in the locale's format, or picked on a one- or two-month calendar with a live preview of the range, plus quick picks such as "Last 7 days". Enforces min/max dates and a minimum and maximum length. The value is `{ start, end }` as ISO `YYYY-MM-DD`, inclusive.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `clearable` | `boolean` | Show a clear button while there is a value. Default true. |
| `defaultValue` | `DateRange \| null` | Initial range (uncontrolled). |
| `disabled` | `boolean` |  |
| `endName` | `string` | Form field name for the end date. |
| `getDayInfo` | `((date: IsoDate) => MiniMonthDayInfo \| undefined)` | Dots and descriptions for calendar days (see MiniMonth). |
| `id` | `string` |  |
| `invalid` | `boolean` | Marks the field invalid. Inside a Field, the Field's `invalid` is used. |
| `isDateDisabled` | `((date: IsoDate) => boolean)` | Blocks extra dates as a start or an end. |
| `locale` | `string` | BCP 47 locale. Default: the runtime's. Pass it for server rendering. |
| `max` | `string` | Latest allowed date. |
| `maxDays` | `number` | Longest allowed range in days, counting both ends (e.g. 366 for reports). |
| `messages` | `Partial<DateRangePickerMessages>` | Text for translation. |
| `min` | `string` | Earliest allowed date. |
| `minDays` | `number` | Shortest allowed range in days, counting both ends. |
| `numberOfMonths` | `1 \| "auto" \| 2` | Months shown in the calendar. `'auto'` (default) shows two on wide screens and one on narrow. |
| `onOpenChange` | `((open: boolean) => void)` |  |
| `onValidationChange` | `((error: DateRangeValidationError \| null) => void)` | Called when the built-in validation result changes (`null` when valid). |
| `onValueChange` | `((value: DateRange \| null) => void)` | Called with a complete, valid range, or `null` when cleared. |
| `open` | `boolean` | Whether the calendar is open (controlled). |
| `presets` | `readonly DateRangePreset[]` | Quick picks such as "Last 7 days", shown beside the calendar. |
| `readOnly` | `boolean` |  |
| `required` | `boolean` |  |
| `showErrors` | `boolean` | Render the built-in error message. Default true. |
| `showWeekNumbers` | `boolean` | Show ISO week numbers in the calendar. |
| `size` | `"sm" \| "md" \| "lg"` | Control height. Default `md`. |
| `startName` | `string` | Form field name for the start date (`YYYY-MM-DD`). |
| `today` | `string` | Today's date. Default: the device's. |
| `value` | `DateRange \| null` | The range (controlled), or `null` for none. |
| `weekStartsOn` | `Weekday` | First day of the calendar week. Default: from the locale. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `numberOfMonths` | `'auto'``'1'``'2'` | `auto` shows two months from 640px wide, one below. Default `auto`. |
| `presets` | `'none'``'list'` | Quick picks beside the calendar (above it on phones). |
| `size` | `'sm'``'md'``'lg'` | Control height. Default `md`. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Picking | `data-preview on calendar cells` | After the first day: the start is marked and the range previews to the hovered or focused day. |
| Selected range | `aria-selected="true", data-in-range` | A tinted band with filled ends. |
| Invalid | `aria-invalid="true", data-invalid` | A date error, end before start, or a range too short or too long; one message explains it. |
| Disabled | `disabled, data-disabled` | Both inputs and the calendar button. |

## Interaction behaviour

- Typed dates are read on blur and Enter; the value changes once both ends are valid.
- In the calendar, the first pick sets the start, the second the end; picking an earlier day second swaps them. The calendar then closes and focus returns to the start field.
- After the first pick a polite announcement says to choose the end date.
- A preset sets both ends at once and closes the calendar; the current preset is pressed.
- Escape closes the calendar and discards a half-picked range.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Start field, end field, clear button, calendar button. |
| ↓ (in a field) | Open the calendar. |
| Arrows, Home/End, Page Up/Down | Move in the calendar; the preview follows focus. |
| Enter / Space | Pick the start, then the end. |
| Escape | Close the calendar. |

## Accessibility

Role

group of two textboxes, with a button that opens a dialog containing one or two grids

Accessible name

The group is named by FieldLabel (or

aria-label

); the inputs are "<label> start date" and "<label> end date"; the button "Choose dates" or "Change dates, <start> to <end>".

- Every day in the range is `aria-selected`; preview days are not, so only the real selection is announced as selected.
- Days blocked by the length limits are `aria-disabled` while picking.
- Presets are toggle buttons in a labelled group.
- One error message, linked to both inputs and announced politely.

## Responsive behaviour

- The box fills its container; each input shrinks evenly.
- Two months side by side from 640px; one month below, so it fits a 320px screen.
- Below 640px, presets move above the calendar as wrapping chips.

## Theme support

- Input tokens for the box and MiniMonth tokens for the calendar; the range band is accent-subtle.
- Forced-colours mode outlines the range and the pressed preset; reduced motion removes transitions.

## Do and don't

### Do

- Send `start` and `end` as inclusive dates; if the API wants an exclusive end, add one day on the way out.
- Set `maxDays` when the backend limits the range, so the calendar blocks it up front.
- Offer presets for the periods people use most.

### Don't

- Do not run a query on every keystroke; `onValueChange` only fires for a complete, valid range.
- Do not convert the dates to local-time `Date` objects to compute days; the strings are calendar dates.

## Related components

- [DatePicker](https://monarchui.dev/components/date-picker/): A single date.
- [MiniMonth](https://monarchui.dev/components/mini-month/): The calendar grid it uses.
- [FilterBar](https://monarchui.dev/components/filter-bar/): Combine the range with other filters.
- [SegmentedControl](https://monarchui.dev/components/segmented-control/): A few fixed periods without a custom range.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Field](https://monarchui.dev/components/form-field/), [Popover](https://monarchui.dev/components/popover/), [MiniMonth](https://monarchui.dev/components/mini-month/) |

## Testing coverage

- Labelled group with start and end inputs and a dialog button; axe clean.
- Typed ranges commit only when both ends are valid; end before start and length errors.
- Calendar picking with hover preview, closing and returning focus; keyboard picking with swap and focus-driven preview.
- Days outside the length limits disabled while picking.
- Presets (`lastDays`, `nextDays`, `months`, `range`), disabled when they break limits, pressed when current.
- min/max dates; two months with a range across them.
- Clear, controlled value, hidden inputs, disabled.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/date-range-picker and the open calendar (overlays/date-range-picker-*.png); Cross-browser axe: /components/date-range-picker |

Tests: `packages/react/src/components/search/date-range-picker`

## All example code

### Report period with presets

```tsx
import { useState } from 'react';
import { Field, FieldLabel, FieldDescription } from '@rookcom/monarch-react';
import { DateRangePicker, type DateRange } from '@rookcom/monarch-react/scheduling';

const TODAY = '2026-11-04';

function ReportRange() {
  const [value, setValue] = useState<DateRange | null>({ start: '2026-10-06', end: '2026-11-04' });
  return (
    <Field>
      <FieldLabel>Report period</FieldLabel>
      <DateRangePicker
        locale="en-GB"
        today={TODAY}
        max={TODAY}
        maxDays={366}
        presets={[
          { label: 'Last 7 days', lastDays: 7 },
          { label: 'Last 30 days', lastDays: 30 },
          { label: 'Last 90 days', lastDays: 90 },
          { label: 'This month', months: 0 },
          { label: 'Last month', months: -1 },
        ]}
        value={value}
        onValueChange={setValue}
      />
      <FieldDescription>
        {value ? `${value.start} to ${value.end}` : 'No period'}; at most 366 days.
      </FieldDescription>
    </Field>
  );
}

<ReportRange />
```

### Leave with a minimum length

```tsx
import { Field, FieldLabel } from '@rookcom/monarch-react';
import { DateRangePicker } from '@rookcom/monarch-react/scheduling';

const TODAY = '2026-11-04';

<Field>
  <FieldLabel>Leave</FieldLabel>
  <DateRangePicker
    locale="en-GB"
    today={TODAY}
    min={TODAY}
    minDays={2}
    defaultValue={{ start: '2026-11-12', end: '2026-11-12' }}
  />
</Field>
```

### Compact filter

```tsx
import { DateRangePicker } from '@rookcom/monarch-react/scheduling';

const TODAY = '2026-11-04';

<DateRangePicker
  aria-label="Created"
  size="sm"
  locale="en-US"
  today={TODAY}
  defaultValue={{ start: '2026-11-01', end: '2026-11-30' }}
/>
```
