Date range picker
ProA button that opens a list of time windows ("Last 24 hours", "Last 7 days") and a Custom one picked on two calendars. Nothing is chosen until the person picks. Each preset prints the exact window beside its words, worked out from now, and the button reads the preset and its window, so nobody has to guess what "last 7 days" covered.
A preset is a rolling window: the value keeps its preset id, and its from and to are the instants when it was picked. Work the window out again from your own clock when you query. A custom window runs from the start of its first day to the end of its last, or between the times picked with withTime, in LocaleProvider's timeZone when it has one and the device's otherwise; Apply stays locked, saying why, until both days are picked and the end is not before the start.
With available, the span the data covers, a window wider than the data is narrowed to it and the picker says so under the button: the value's from and to are what's shown, and requested is what was asked for (with its preset, which the narrowed value no longer carries); the button prints the narrowed range without the preset's words. The same goes for a window that reaches the picker wider than the data, as a controlled value, a defaultValue, or when available narrows after a pick: the picker shows it narrowed, with the same note, and leaves the value you hold as it is until the person picks again.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1 and 2.6.2: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/date-range-picker
Brings in: button, calendar, locale, locked, popover, time-input, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { DateRangePicker, type DateRangeValue } from "@/components/candor/ui/date-range-picker"
// Runs are kept for 14 days: a wider window is narrowed to them, and the picker says so.
const kept = 14 * 24 * 60 * 60 * 1000
// Each preset prints the exact window beside its words. A preset keeps its id, so work the rolling window out again
// from your own clock when you query. A preset narrowed to the data keeps its id on `requested` instead.
export default function Example() {
const [picked, setPicked] = React.useState<DateRangeValue | undefined>(undefined)
const [available] = React.useState(() => {
const now = new Date()
return { from: new Date(now.getTime() - kept), to: now }
})
return (
<div className="flex flex-col items-start gap-2">
<DateRangePicker aria-label="Runs started" value={picked ?? null} onValueChange={setPicked} available={available} withTime clearable />
<p className="text-sm text-muted-foreground">
{!picked
? "No window chosen: every run."
: picked.preset
? `Rolling window: ${picked.preset}`
: picked.requested?.preset
? `Asked for ${picked.requested.preset}, narrowed to the 14 days kept`
: "A fixed window"}
</p>
</div>
)
}<DateRangePicker>
| Prop | Type | Default | Description |
|---|---|---|---|
| value | DateRangeValue | null | undefined | — | The chosen window, when you control it. `null` is nothing chosen. |
| defaultValue | DateRangeValue | undefined | — | The window chosen at first, when you don't control it. Left out, nothing is chosen. |
| onValueChange | ((value: DateRangeValue | undefined) => void) | undefined | — | Called with the window a person picks, or `undefined` when they clear it. |
| presets | DateRangePreset[] | undefined | — | The rolling windows to offer. Defaults to the last hour, 24 hours, 7 days and 30 days. An empty list offers only Custom. |
| custom | boolean | undefined | true | Offers a Custom window picked on two calendars. Defaults to true. |
| withTime | boolean | undefined | false | A custom window takes a start and end time as well as days. |
| clearable | boolean | undefined | false | Shows a "Clear" button once a window is chosen. |
| available | DateRange | undefined | — | The span the data covers. A window picked wider than it is narrowed to it (the value keeps the one asked for as `requested`), and the picker says "Data exists from … to …; showing that." under the button and in the list. A `value` or `defaultValue` wider than it, or one left wider when it narrows, is shown narrowed with the same note, without a call to `onValueChange`. |
| now | (() => Date) | undefined | — | The clock presets are worked out from. Defaults to the device's time when the list opens; pass your server's for the same answer everywhere. |
| locked | string | null | undefined | — | Why the picker can't be used right now. The button stays focusable and says why. |
| placeholder | string | undefined | — | Text on the button while nothing is chosen. Defaults to "Pick a time window". |
| id | string | undefined | — | The button's id, for a `<label htmlFor>` or `Field` to point at. |
| className | string | undefined | — | Classes for the button. |
| "aria-label" | string | undefined | — | The button's accessible name when it needs one beyond its text (the field's label). |
| "aria-describedby" | string | undefined | — | Ids of the text that describes the window: a hint, an error. `Field` sets it. |
| "aria-invalid" | boolean | "true" | "false" | undefined | — | The window has an error: the button is outlined in the destructive colour. `Field` sets it. |
| "aria-required" | boolean | "true" | "false" | undefined | — | A window must be chosen. `Field` sets it from `required`. |
Functions
clampRange(value: DateRangeValue, available: DateRange | undefined)A window narrowed to the data that exists: from and to inside available, with the window asked for (and its preset) kept as requested and preset dropped, since the narrowed window isn't the preset's. A window inside it, or one that holds none of it (narrowing would turn it inside out), is left as it is.
useDefaultRangePresets()The presets used when you give none, in the locale's words: the last hour, 24 hours, 7 days and 30 days.
useDateRangeText()A window as text in the locale: "2 Nov 2026 – 9 Nov 2026", with times when the window is shorter than two days or doesn't start and end on whole days. Pass a preset's label to lead with it: "Last 7 days: 2 Nov 2026 – 9 Nov 2026".
Strings
The fixed words, in English by default. Change any of them under dateRangePicker in <LocaleProvider strings={{ … }}>; the table is exported as dateRangePickerStrings.
| Key | English |
|---|---|
| placeholder | Pick a time window |
| presets | Time windows |
| lastHour | Last hour |
| last24Hours | Last 24 hours |
| last7Days | Last 7 days |
| last30Days | Last 30 days |
| custom | Custom |
| from | From |
| to | To |
| startTime | Start time |
| endTime | End time |
| apply | Apply |
| clear | Clear |
| range | {from} – {to} |
| labelled | {label}: {range} |
| needBoth | Pick a start and an end day first. |
| endBeforeStart | The end is before the start. |
| clamped | Data exists from {from} to {to}; showing that. |
| availableSpan | Data exists from {from} to {to}. |
| outside | No data exists in this window; data exists from {from} to {to}. |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | A preset's background changes when the pointer is over it, and a ring fades in when it takes keyboard focus. |
Set with --cd-motion-date-range-picker-<moment>-* in your brand file; the families are in the design reference.
Types
- DateRange
- A window of time, from one instant to another.
{ /** The start, included. */ from: Date; /** The end, included. */ to: Date; }- DateRangeValue
- A chosen window: the instants, and the preset it came from when it was one.
DateRange & { /** * The preset's `id` when the window is a rolling one ("Last 7 days"), so the app can work it out again from its own * clock. Absent for a custom window, and for one narrowed to the data: `from` and `to` are then no longer the * preset's window, so the preset is kept on `requested` instead. */ preset?: string | undefined; /** * The window the person asked for, with its `preset` when it was one, when it was wider than the picker's `available` * data and `from` and `to` were narrowed to it. Absent when nothing was narrowed. */ requested?: (DateRange & { preset?: string | undefined; }) | undefined; }- DateRangePreset
- A rolling window that ends now: "Last 24 hours".
{ /** Kept on the value as `preset`. */ id: string; /** The words: "Last 24 hours". */ label: string; /** How far back the window starts, in milliseconds. */ duration: number; }