Skip to the content

ComponentsForm

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: pnpm add react-day-picker

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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 DatePickers or DateFields where typing matters.
  • Below 640 px the presets wrap above the calendar and the months stack.

Keyboard

Keyboard
KeyBehaviour
EnterOn the field: opens the popup. On a preset: chooses it.
Alt↓On the field: opens the popup with focus on the calendar.
EscapeCloses the popup without a change and returns focus to the field.
TabMoves 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.

DateRangePicker props
PropTypeDefaultDescription
calendarPropsDateRangePickerCalendarPropsProps for the Calendar inside.
clearablebooleanfalseShows a button that clears the range while there is one.
defaultMonthDateThe first month shown when nothing is chosen. Defaults to today's.
defaultOpenboolean
defaultValueDateRangeValue | nullThe range it starts with, uncontrolled.
fluidbooleanfalseFills the width of its container.
maxDateThe latest day that can be chosen.
minDateThe earliest day that can be chosen.
numberOfMonthsnumber2How 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.
openboolean
placeholderstringText in the field while no range is chosen.
presetsboolean | DateRangePreset[]trueThe 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.
showActionsbooleanfalseAdds 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.
todayDateThe day the presets count from, and the one marked as today. Defaults to the system date.
valueDateRangeValue | nullThe 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.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--accentbackground
--accent-foregroundtext
--controlborder
--control-hoverborder, text
--destructive-strongtext
--fieldbackground
--field-disabledbackground
--field-disabled-foregroundtext
--field-filledbackground
--foregroundtext
--highlightbackground
--highlight-focusbackground
--highlight-foregroundtext
--invalidborder
--muted-foregroundtext
--ringborder, outline