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
buttonwithrole="combobox",aria-haspopup="dialog",aria-expandedandaria-controls; its text is the chosen range or the placeholder. The popup is a non-modaldialognamed "Choose dates" holding the presets (toggle buttons witharia-pressedon the one that matches the range) and the rangeCalendargrid (see Calendar). Cancel and Apply are buttons. - Labels
- Name the field with a
Label htmlFororaria-label. The popup, the presets, Cancel, Apply and the clear button are named by the provider stringschooseDateRange,presetToday…presetLastMonth,cancel,applyandclear. - 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 orDateFields where typing matters. - Below 640 px the presets wrap above the calendar and the months stack.
- The calendar follows the provider's
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.
The tokens its classes read. Change their values in your theme, and it follows.
| 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 |