RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Components

  • Navigation

    • Breadcrumb
    • LinkBeta
    • NavigatorAlpha
    • Steps
    • Tabs
  • Layout

    • Accordion
    • CollapsibleBeta
    • PaneAlpha
    • ScrollArea
    • Separator
  • Actions

    • Button
    • IconButton
    • ToggleBeta
    • Toggle groupBeta
    • ToolbarBeta
  • Fields

    • EditorAlpha
    • Field
    • Fieldset
    • Input
    • Label
    • Number fieldBeta
    • OTP fieldBeta
    • Query fieldAlpha
    • Textarea
  • Choices

    • Autocomplete
    • CheckboxBeta
    • Combobox
    • Radio group
    • Select
    • SliderBeta
    • SwitchBeta
  • Date and time

    • CalendarBeta
    • CalendarTile
    • Countdown
    • Dashboard periodBeta
    • Date fieldBeta
    • Date pickerBeta
    • Date range pickerBeta
    • DateTime
    • Duration
    • Time fieldBeta
  • Collections

    • Card
    • Carousel
    • DataTableBeta
    • ListAlpha
    • Marquee
    • RecordGridAlpha
    • RecordTableAlpha
    • SortableAlpha
    • TableBeta
  • Overlays

    • Dialog
    • Drawer
    • MenuBeta
    • Popover
    • ToastBeta
    • Tooltip
  • Status

    • Badge
    • CalloutBeta
    • DeltaBeta
    • Empty state
    • MeterBeta
    • ProgressBeta
    • SkeletonBeta
    • SparklineBeta
    • StatTileBeta
  • Media & brand

    • AvatarBeta
    • Icon Tile
    • Image
    • Logo
    • PatternBeta
    • QR codeBeta
    • SpotIllustration
  • Text

    • Code
    • Highlight
    • KbdBeta
    • Mark
    • Prose

Date range picker

Status
Beta
Added
Components 2.16.020 Oct 2026
Docs updated
19 Oct 2026

A button showing a date range, opening presets, typed dates and a calendar.

Import

import { DateRangePicker } from '@oztix/roadie-components/date-range-picker'

Examples

Default

Press the button and choose a preset, type a start and an end, or press two days in the calendar. The Start and End fields suggest single dates as you type, as DateField does. They never suggest a range, since the presets hold those, and End suggests nothing before the start. A preset keeps its words, so "Last 30 days" saved today still means the last 30 days next week, and arrives in its one form: { period: 'week', offset: -1 } as 'last-week'. Typed or pressed dates become an absolute range, { start, end }, with both ends included. The button shows the range in words, with the dates a relative range stands for beside it.

function Default() {
const [range, setRange] = useState({ direction: 'past', amount: 30, unit: 'day' })
return (
<div className='grid max-w-96 gap-4'>
<DateRangePicker aria-label='Sales period' value={range} onValueChange={setRange} />
<p className='text-sm text-subtle'>Value: {JSON.stringify(range)}</p>

Variants

commit='apply' holds every change until Apply is pressed, and Cancel or Escape drops them. Use it when a change refetches a whole page, such as a dashboard's period. The default, immediate, changes the value with each choice, as a filter does.

function Apply() {
const [range, setRange] = useState('last-month')
return (
<div className='grid max-w-96 gap-4'>
<DateRangePicker aria-label='Dashboard period' commit='apply' value={range} onValueChange={setRange} />
<p className='text-sm text-subtle'>Value: {JSON.stringify(range)}</p>

granularity='minute' adds an optional time to each end, read on the clock of timeZone. An end with a time becomes an instant with its offset, such as '2026-11-02T09:00:00+11:00'; an end with no time covers its whole day.

function Presale() {
const [range, setRange] = useState({ start: '2026-11-02T09:00:00+11:00', end: '2026-11-04' })
return (
<div className='grid max-w-96 gap-4'>
<Field>
<Field.Label>Presale, Sydney time</Field.Label>

Emphasis

<DateRangePicker aria-label='Normal' defaultValue='last-week' />
<DateRangePicker aria-label='Subtle' emphasis='subtle' defaultValue='last-week' />
<DateRangePicker aria-label='Subtler' emphasis='subtler' defaultValue='last-week' />

Sizes

<DateRangePicker aria-label='Small' size='sm' defaultValue='last-week' />
<DateRangePicker aria-label='Medium' size='md' defaultValue='last-week' />
<DateRangePicker aria-label='Large' size='lg' defaultValue='last-week' />

States

readOnly shows the range with a lock and doesn't open, for a period the page has set. disabled turns the button off.

{/* Default */}
<DateRangePicker aria-label='Default' defaultValue='last-week' />
{/* Invalid */}
<DateRangePicker aria-label='Invalid' defaultValue='last-week' invalid />
{/* Read only */}
<DateRangePicker aria-label='Read only' defaultValue='last-week' readOnly />

Composition

With Field

Wrap it in Field. The label names the button, the helper or error text describes it, and required adds “Required” to its description. Typed start and end dates explain their own errors inside the popup. The value is an object, so the picker holds it as state and submits nothing with a form; send it from your own handler.

<Field className='max-w-80'>
<Field.Label>Sales period</Field.Label>
<DateRangePicker />
<Field.HelperText>Orders placed in this period</Field.HelperText>
</Field>

With a comparison

For a dashboard, use Dashboard period: this picker with a Compare switch under the range, applied with it. Elsewhere, put a separate control beside the picker, such as a Select. Resolve both with resolveDateRange and resolveComparison from @oztix/roadie-core/datetime, passing the picker's timeZone, weekStart and fiscalYearStart, so they agree on what the words mean. A benchmark that isn't a date, such as similar venues, belongs in its own control.

function Toolbar() {
const [range, setRange] = useState({ direction: 'past', amount: 30, unit: 'day' })
const [compare, setCompare] = useState('previous-period')
return (
<div className='flex flex-wrap items-center gap-2'>
<DateRangePicker aria-label='Period' size='sm' commit='apply' className='w-fit' value={range} onValueChange={setRange} />

With presets

presets replaces the list, and group sets presets under a heading. Presets with no group come first, then each group in the order it first appears. Spread dateRangePresets to add to the defaults instead. A preset's label defaults to its value in words. When the value is a preset, fixed dates or relative, the button shows the preset's label with its dates, and so does the list on a phone. Here Xylophone Tide Festival adds its on-sale week, and a dashboard keeps the periods it reports on.

<DateRangePicker
aria-label='Festival period'
defaultValue={{ start: '2026-11-02', end: '2026-11-08' }}
presets={[
...dateRangePresets,
{ label: 'On-sale week', value: { start: '2026-11-02', end: '2026-11-08' }, group: 'Xylophone Tide Festival' }

With disabled days

disabled takes the same matchers as Calendar. Matched days are disabled in the calendar and refused when typed. min and max limit the length in days, both ends counted, and typed dates that break them say so. Presets aren't checked against either, so leave out presets that reach matched days or break the length. Here sales data starts on 1 July 2026.

<DateRangePicker
aria-label='Sales period'
className='max-w-80'
disabled={{ before: '2026-07-01' }}
startMonth='2026-07-01'
/>

Guidelines

Keep relative ranges relative

saveView({ period: range })

Do

Save the value as it comes. "Last 30 days" stays the last 30 days, and the button shows the dates it stands for today.

saveView({ period: resolveDateRange(range, options) })

Don’t

Don't save resolved dates for a relative choice. The view stops moving with time.

Let a phone show one thing at a time

Do

Keep presets as you'd show them on a wide screen. On a phone the picker puts them on their own Periods tab, each with its dates, and the calendar on another.

Don’t

Don't build a separate phone picker from chips or a second calendar. The drawer already gives each its own room.

Apply when a change is expensive

Do

Use commit='apply' for a dashboard period, where every card refetches.

Don’t

Don't make a filter wait for Apply. Filters change the list as each choice is made.

Accessibility

  • The button is named after the picker and its range, such as "Choose dates, Last 30 days 8 Sept to 7 Oct 2026, Sales period", so pickers side by side stay apart. The range reads as the button shows it, so voice control users can say what they see. The popup is named "Choose dates, Sales period".
  • Opening moves focus to the chosen preset, or to the calendar's first chosen day. In the drawer, the keyboard goes from Close to the tabs, then on the Calendar tab to Start and End, then into the scrolling body and its periods or months, then Clear and Apply. Escape closes and puts focus back on the button. With commit='apply' it drops the changes; in immediate mode, dates already chosen or committed stay.
  • Presets are Toggle buttons with aria-pressed, in groups named by their headings. Custom range is pressed when the value is no preset, and moves focus to the start date.
  • Start and End are comboboxes, text inputs with suggestions, each with its own label and errors. Focused empty, they show hints with nothing highlighted. Once you type, the first suggestion is highlighted and Enter takes it, and Escape closes the list and keeps the text. In the drawer the list opens above it, so nothing clips it.
  • With no suggestion highlighted, Enter commits typed text, and Escape puts back the last date. An end before the start says "Ends before it starts".
  • The calendar works as in Calendar: arrow keys, Page Up, Page Down, Home and End move, and Enter or Space chooses. Escape after the first day of a range drops it and keeps the dates you had.
  • On a screen narrower than 48rem, the popup is a full-height bottom Drawer with the same name, titled with the picker's label. Under the title, its description sums up the dates chosen, such as "8 Sept to 7 Oct 2026 · 30 days", so it is read as the drawer opens.
  • Two Tabs, Periods and Calendar, switch what the drawer shows. It opens on Periods when the value is a preset, and on Calendar otherwise. Periods is a List with a row for each preset, its dates beside it, under its group's heading. The chosen row has aria-current and a check.
  • Calendar has Start and End at the top, then months that scroll under a pinned row of weekdays, filling the width. The field marked in accent is the end the next tapped day sets: End while only a start is chosen, otherwise Start, which begins a new range. Tap End to move only the end, or Start to move only the start. An aimed end or start moves alone, even where it puts the end before the start or breaks min or max, and the End field says why.
  • With commit='apply', Clear and Apply stay in view at the drawer's foot. Close, Escape, a tap outside or a swipe down closes it, and with commit='apply' drops the changes, as Cancel does in the popover.

API reference

DateRangePicker

A button showing a date range, opening presets, typed dates and a calendar.

valueDateRangeValue | null

The range: absolute `{ start, end }` with inclusive ISO dates (or date-times at `minute` granularity), or a relative range such as `'last-week'` or `{ direction: 'past', amount: 30, unit: 'day' }`. Pair with `onValueChange`.

defaultValueDateRangeValue | null

The range to start from when uncontrolled.

onValueChange((value: DateRangeValue | null) => void)

Called with a preset's value, relative and in its one form (`canonicalDateRange`), or with absolute dates when they are chosen or typed. Null when both dates are cleared.

presetsreadonly DateRangePreset[]

Choices beside the calendar, grouped by their `group`. Pass your own, or add to `dateRangePresets`. An empty list hides it.

Defaults to dateRangePresets.

commit"immediate" | "apply"

`immediate` changes the value with each choice. `apply` holds changes until Apply is pressed, for a period that refetches a whole page.

Defaults to 'immediate'.

granularity"day" | "minute"

`minute` adds an optional time to each end. An end with no time covers its whole day.

Defaults to 'day'.

timeZonestring

IANA zone whose calendar decides today and whose wall clock times are read on. Defaults to the viewer's.

todaystring

Today as an ISO date. Defaults to today in `timeZone`.

weekStartnumber

First day of the week, for the calendar and "last week": 1 is Monday and 7 is Sunday.

Defaults to 1.

fiscalYearStartnumber

The month the financial year opens, for financial presets.

Defaults to 7.

localestring

Defaults to 'en-AU'.

disabledboolean | CalendarMatchers

`true` turns the picker off. Matchers, as on `Calendar`, disable those days in the calendar and refuse them when typed. Inherits `true` from `Field`.

invalidboolean

Marks the picker as invalid. Inherits from `Field` when omitted.

readOnlyboolean

Shows the range with a lock, without letting it change.

requiredboolean

With `commit='apply'`, keeps Apply off while both dates are empty. Inherits from `Field` when omitted.

size"sm" | "md" | "lg"

Defaults to 'md'.

emphasis"normal" | "subtle" | "subtler"

Defaults to 'normal'.

placeholderstring

Shown while no range is chosen.

Defaults to 'Choose dates'.

numberOfMonthsnumber

Months side by side. Defaults to two on wide screens and one on narrow.

minnumber

The fewest days the range can span, both ends counted.

maxnumber

The most days the range can span, both ends counted.

captionLayout"label" | "dropdown"

`dropdown` swaps the calendar's month name for month and year selects.

Defaults to 'label'.

startMonthstring

The earliest month the calendar reaches, as any ISO date in it.

endMonthstring

The latest month the calendar reaches, as any ISO date in it.

hourCycle12 | 24

At `minute` granularity, `12` reads "7:30pm" and `24` reads "19:30".

Defaults to 12.

minuteStepnumber

At `minute` granularity, minutes per step of the time fields' arrow keys.

Defaults to 1.

openboolean

Whether the popup is open. Pair with `onOpenChange`.

defaultOpenboolean

Whether the popup starts open when uncontrolled.

onOpenChange((open: boolean) => void)

Called when the popup opens or closes.

Previous page← Date pickerNext pageDateTime →