# Date range picker

Chooses a first and a last day in one field, from a two-month calendar or a list of presets.

- **Import:** `import { DateRangePicker } from "@booleanpress/ui/date-range-picker"`
- **Also install:** `react-day-picker`
- **APG Date picker combobox:** <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-datepicker/>
- **Page:** <https://ui.booleanpress.com/components/date-range-picker> · @booleanpress/ui 0.2.0

## Usage

`DateRangePicker` is a field-like button that opens the package's `Calendar` in range mode, two months side by side, in a `Popover`, with a list of presets beside it. It needs the `react-day-picker` peer package. Name it with a `Label` whose `htmlFor` is its `id`.

```tsx
import { useState } from "react"
import { DateRangePicker, type DateRangeValue } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export function ReportPeriod() {
  const [range, setRange] = useState<DateRangeValue | null>(null)
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="report-period">Report period</Label>
      <DateRangePicker id="report-period" value={range} onValueChange={setRange} placeholder="All time" />
    </div>
  )
}
```

It is uncontrolled with `defaultValue`, or controlled with `value` and `onValueChange`. The value is `{ from, to }`, both midnight of their day in the provider's `timeZone`, `to` included; null is no range. The field shows it in the provider's `locale` ("Oct 1 – 7, 2026"). The first click on the calendar starts a new range and the second ends it; the range is then chosen and the popup closes. `showActions` adds Cancel and Apply: a choice then waits for Apply, and Cancel, Escape or a click outside leaves the value as it was.

**Presets.** Today, Yesterday, Last 7 days, Last 30 days, This month (the 1st to today) and Last month are counted from `today`, in the provider's time zone; their labels are provider strings. Pass your own list (`{ label, range: (today) => ({ from, to }) }`) or `presets={false}`. A preset outside `min` and `max` is disabled.

**Time zones.** As in `DatePicker`: a day is midnight of that day in the provider's `timeZone`, so a site in "Europe/Berlin" gets Berlin's days whatever the visitor's clock. To query "up to the end of `to`", add one day to `to` in that zone. `name` submits `2026-10-01/2026-10-07` in a hidden input.

`size`, `variant="filled"`, `fluid`, `clearable` and `disabled` follow the other fields; the field is as wide as its text unless `className` or `fluid` sets a width. Pass `today` for screenshots and tests that must not read the clock.

## Examples

### Basic

A named range with no presets; two clicks on the calendar choose it.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerBasic() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="report-period">Report period</Label>
      <DateRangePicker
        id="report-period"
        presets={false}
        defaultValue={{ from: new Date(2026, 9, 1), to: new Date(2026, 9, 7) }}
        today={new Date(2026, 9, 14)}
        placeholder="Choose dates"
        className="w-64"
      />
    </div>
  )
}
```

### Presets

Today, Yesterday, Last 7 days, Last 30 days, This month and Last month beside the calendar, counted from `today`.

```tsx
import { useState } from "react"
import { DateRangePicker, type DateRangeValue } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerPresets() {
  const [range, setRange] = useState<DateRangeValue | null>(null)

  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="delivery-logs">Delivery logs</Label>
      <DateRangePicker
        id="delivery-logs"
        value={range}
        onValueChange={setRange}
        today={new Date(2026, 9, 14)}
        placeholder="All time"
        className="w-64"
      />
    </div>
  )
}
```

### Apply and cancel

`showActions` adds Cancel and Apply: the range waits for Apply.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerActions() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="export-range">Export tickets created</Label>
      <DateRangePicker
        id="export-range"
        showActions
        defaultValue={{ from: new Date(2026, 8, 15), to: new Date(2026, 9, 14) }}
        today={new Date(2026, 9, 14)}
        className="w-64"
      />
    </div>
  )
}
```

### Min and max

`min` and `max` keep the range within the last 30 days; the presets that reach outside are disabled.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerMinMax() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="retained-logs">Logs kept for 30 days</Label>
      <DateRangePicker
        id="retained-logs"
        min={new Date(2026, 8, 15)}
        max={new Date(2026, 9, 14)}
        today={new Date(2026, 9, 14)}
        placeholder="Choose dates"
        className="w-64"
      />
    </div>
  )
}
```

### Clear

`clearable` shows a × while a range is chosen; it clears it and keeps the focus on the field.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerClear() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="campaign-dates">Campaign dates</Label>
      <DateRangePicker
        id="campaign-dates"
        clearable
        defaultValue={{ from: new Date(2026, 9, 19), to: new Date(2026, 9, 25) }}
        today={new Date(2026, 9, 14)}
        placeholder="No dates"
        className="w-64"
      />
    </div>
  )
}
```

### Sizes

`sm` is 28 px tall with 12 px text, `default` 35 px with 14 px, `lg` 42 px with 16 px.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"

const week = { from: new Date(2026, 9, 5), to: new Date(2026, 9, 11) }
const today = new Date(2026, 9, 14)

export default function DateRangePickerSizes() {
  return (
    <div className="flex flex-col items-center gap-3">
      <DateRangePicker size="sm" aria-label="Small" defaultValue={week} today={today} className="w-56" />
      <DateRangePicker aria-label="Default" defaultValue={week} today={today} className="w-60" />
      <DateRangePicker size="lg" aria-label="Large" defaultValue={week} today={today} className="w-64" />
    </div>
  )
}
```

### Filled

`variant="filled"` draws the grey `--field-filled` fill.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerFilled() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="billing-period">Billing period</Label>
      <DateRangePicker
        id="billing-period"
        variant="filled"
        defaultValue={{ from: new Date(2026, 9, 1), to: new Date(2026, 9, 31) }}
        today={new Date(2026, 9, 14)}
        className="w-64"
      />
    </div>
  )
}
```

### Disabled

The field cannot be opened.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerDisabled() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="trial-period">Trial period</Label>
      <DateRangePicker
        id="trial-period"
        disabled
        defaultValue={{ from: new Date(2026, 9, 1), to: new Date(2026, 9, 14) }}
        today={new Date(2026, 9, 14)}
        className="w-64"
      />
    </div>
  )
}
```

### Invalid

`aria-invalid` draws the error edge and a red placeholder; `aria-describedby` reads the message.

```tsx
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerInvalid() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="audit-period">Audit period</Label>
      <DateRangePicker
        id="audit-period"
        aria-invalid
        aria-describedby="audit-period-error"
        today={new Date(2026, 9, 14)}
        placeholder="Choose dates"
        className="w-64"
      />
      <p id="audit-period-error" className="text-sm text-destructive-strong">
        Choose the first and last day to audit.
      </p>
    </div>
  )
}
```

### With a form

`name` submits the range as `YYYY-MM-DD/YYYY-MM-DD`, empty when cleared; the form's Reset puts `defaultValue` back.

```tsx
import * as React from "react"
import { Button } from "@booleanpress/ui/button"
import { DateRangePicker } from "@booleanpress/ui/date-range-picker"
import { Label } from "@booleanpress/ui/label"

export default function DateRangePickerWithForm() {
  const [sent, setSent] = React.useState("")

  return (
    <form
      className="flex w-full max-w-sm flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        setSent([...new FormData(event.currentTarget)].map(([key, value]) => `${key} = ${value}`).join(", "))
      }}
    >
      <div className="flex flex-col gap-2">
        <Label id="report-period-label">Report period</Label>
        <DateRangePicker
          aria-labelledby="report-period-label"
          name="range"
          clearable
          defaultValue={{ from: new Date(2026, 9, 5), to: new Date(2026, 9, 7) }}
        />
      </div>
      <div className="flex gap-2">
        <Button type="submit">Submit</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </div>
      <pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 font-mono text-xs whitespace-pre-wrap text-foreground">{sent ? `Sent: ${sent}` : "Press Submit to see what the form sends."}</pre>
    </form>
  )
}
```

## Accessibility

**Semantics.** The field is a `button` with `role="combobox"`, `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`; its text is the chosen range or the placeholder. The popup is a non-modal `dialog` named "Choose dates" holding the presets (toggle buttons with `aria-pressed` on the one that matches the range) and the range `Calendar` grid (see Calendar). Cancel and Apply are buttons.

**Labels.** Name the field with a `Label htmlFor` or `aria-label`. The popup, the presets, Cancel, Apply and the clear button are named by the provider strings `chooseDateRange`, `presetToday` … `presetLastMonth`, `cancel`, `apply` and `clear`.

**Focus.** Enter, Space or Alt+ArrowDown on the field opens the popup and moves focus to the range's first day, else today. Tab moves through the presets, the calendar and the buttons, and wraps at the ends. Choosing a range, a preset, Apply, Cancel and Escape close it and return focus to the field; a click outside closes it without moving focus.

**Known limits.**

- The calendar follows the provider's `locale` (see Calendar); without one, its day names are react-day-picker's English.
- The range is chosen with the calendar or a preset; it cannot be typed. Pair two `DatePicker`s or `DateField`s where typing matters.
- Below 640 px the presets wrap above the calendar and the months stack.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Enter | On the field: opens the popup. On a preset: chooses it. |
| Alt + ↓ | On the field: opens the popup with focus on the calendar. |
| Escape | Closes the popup without a change and returns focus to the field. |
| Tab | Moves to the next control of the popup; from the last it wraps to the first. |

## API

### DateRangePicker

Renders a `button` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `calendarProps` | `DateRangePickerCalendarProps` |  | Props for the Calendar inside. |
| `clearable` | `boolean` | `false` | Shows a button that clears the range while there is one. |
| `defaultMonth` | `Date` |  | The first month shown when nothing is chosen. Defaults to today's. |
| `defaultOpen` | `boolean` |  |  |
| `defaultValue` | `DateRangeValue \| null` |  | The range it starts with, uncontrolled. |
| `fluid` | `boolean` | `false` | Fills the width of its container. |
| `max` | `Date` |  | The latest day that can be chosen. |
| `min` | `Date` |  | The earliest day that can be chosen. |
| `numberOfMonths` | `number` | `2` | How many months show side by side. |
| `onOpenChange` | `((open: boolean) => void)` |  |  |
| `onValueChange` | `((value: DateRangeValue \| null) => void)` |  | Called with the new range once both ends are chosen (or on Apply), and with null when it is cleared. |
| `open` | `boolean` |  |  |
| `placeholder` | `string` |  | Text in the field while no range is chosen. |
| `presets` | `boolean \| DateRangePreset[]` | `true` | The list beside the calendar: `true` for Today, Yesterday, Last 7 days, Last 30 days, This month and Last month; your own list; or `false` for none. |
| `showActions` | `boolean` | `false` | Adds Cancel and Apply under the calendar: a choice waits for Apply. |
| `size` | `"default" \| "sm" \| "lg"` |  | 28, 35 or 42 px tall. Defaults to the provider's `controlSize`. |
| `today` | `Date` |  | The day the presets count from, and the one marked as today. Defaults to the system date. |
| `value` | `DateRangeValue \| null` |  | The chosen range, when you control it; null is none. |
| `variant` | `"default" \| "filled"` |  | `filled` fills the field grey. Defaults to the provider's `fieldVariant`. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="date-range-picker"` (DateRangePicker), and `data-size`, `data-variant`, `data-placeholder`.

**Provider strings:** `presetToday`, `presetYesterday`, `presetLast7Days`, `presetLast30Days`, `presetThisMonth`, `presetLastMonth`, `clear`, `chooseDateRange`, `cancel`, `apply` (`BooleanUIProvider`'s `strings`).

## Theming

The field is the field look of `Input` and `Select`; the popup is the popover surface with a 6 px radius and 10 px padding. The pressed preset is `--highlight`; the range ends are `--primary` and the days between `--highlight`, as in `Calendar`.

| Token | Used for |
| --- | --- |
| `--accent` | background |
| `--accent-foreground` | text |
| `--control` | border |
| `--control-hover` | border, text |
| `--destructive-strong` | text |
| `--field` | background |
| `--field-disabled` | background |
| `--field-disabled-foreground` | text |
| `--field-filled` | background |
| `--foreground` | text |
| `--highlight` | background |
| `--highlight-focus` | background |
| `--highlight-foreground` | text |
| `--invalid` | border |
| `--muted-foreground` | text |
| `--ring` | border, outline |
