Date Range Picker
Choose a date range by typing, selecting a preset or using a calendar.
import { DateRangePicker, DateRangePreset } from '@youj/react/date'
Installation & usage
npm i @youj/react date-fns react-day-pickerFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Requires date-fns and react-day-picker, optional dependencies. Import it from @youj/react/date.
Use a date range picker for a period: a reporting window, a freeze, a trip, a billing cycle. Presets cover the common spans, and the calendar covers the rest. For a single date, use a Date Picker. When the range is the focus of the page, show a Calendar in range mode instead.
import { useState } from 'react'
import { Field, FieldLabel } from '@youj/react'
import { DateRangePicker, type DateRange } from '@youj/react/date'
export function ReportingPeriod() {
const [range, setRange] = useState<DateRange | null>(null)
return (
<Field>
<FieldLabel>Reporting period</FieldLabel>
<DateRangePicker value={range} onValueChange={setRange} maxDate={new Date()} />
</Field>
)
}The field holds two inputs, start and end, under one label. Each reads text the way the Date Picker does: the locale's order with any separator, month names, ISO dates. An end typed before the start swaps them. The value is react-day-picker's { from, to }, or null.
The calendar button opens a Popover with presets beside a two-month range Calendar. On a narrow screen it shows one month, with the presets wrapped above it. On touch it's a sheet (see Touch). The footer says how many days are chosen and is announced as it changes. Clicking a day after a full range starts a new one.
Description
A reporting period that starts as the last seven days, with no date after today.
The defaults are Today, Yesterday, Last 7 days, Last 30 days, This month, Last month and Year to date, exported as dateRangePresets. The preset matching the current range carries a pill that glides between presets, and choosing one moves the calendar to it with the month slide, so you see the band land. Pass your own list, or presets={false} to hide them.
import { addDays, startOfWeek } from 'date-fns'
import { dateRangePresets, type DateRangePreset } from '@youj/react/date'
const presets: DateRangePreset[] = [
{ label: 'This sprint', range: (today) => ({ from: startOfWeek(today), to: addDays(startOfWeek(today), 13) }) },
...dateRangePresets.slice(2, 4),
]Each preset's range receives today at the start of the day, in local time.
A freeze window with the presets off, no past dates and no weekends.
minDate, maxDate and disabledDates bound the calendar, the typing and the presets: a range can't span a disabled day, and a preset that falls outside the bounds is disabled. This one plans ahead, with no weekends and no presets.
Code
date-range-picker-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsvalue, defaultValue, onValueChange… (18)
Props
| Prop | Type | |
|---|---|---|
value | DateRange | null | – |
defaultValue | DateRange | nulldefault null | null |
onValueChange | (range: DateRange | null) => void | – |
presets | false | DateRangePreset[]Default: Today, Yesterday, Last 7 / 30 days, This / Last month, Year to date. | – |
locale | LocaleA date-fns / react-day-picker locale. Default: YoujProvider's | – |
minDate | Date | – |
maxDate | Date | – |
disabledDates | Matcher | Matcher[]Days that can't be chosen (react-day-picker matchers); a range can't span them. | – |
numberOfMonths | numberMonths side by side. Default 2 (1 on phones). | – |
disabled | boolean | – |
name | stringAlso submits | – |
id | string | – |
ref | Ref<HTMLInputElement> | – |
onBlur | () => void | – |
className | string | – |
startLabel | stringNames the start and end fields. Default "Start date" / "End date". | – |
endLabel | string | – |
presentation | OverlayPresentation'auto''sheet''popover'
default 'auto' | 'auto' |
DateRangePreset is { label: string; range: (today: Date) => { from: Date; to: Date } }.
TouchOn a touch device the two fields are still text fields, set at 16px
Touch
On a touch device the two fields are still text fields, set at 16px. The calendar button opens a bottom sheet with the presets in one row of 44px chips that scrolls sideways, one month with 44px days, the day count, and a Done button. A range takes two taps, so the sheet stays open until you press Done, pull it down, tap outside it or press Escape.
presentation chooses: auto (the default) is a popover under a mouse or trackpad and a sheet on touch, sheet always uses the sheet, and popover keeps the popover on touch too.
MotionOpen, close, preset pill… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Openthe popover grows out of the field (yj-pop) | duration.fastease.enter140ms · cubic-bezier(0.16, 1, 0.3, 1) | 140mscubic-bezier(0.16, 1, 0.3, 1) | |
| Close | duration.instantease.exit80ms · cubic-bezier(0.4, 0, 1, 1) | 80mscubic-bezier(0.4, 0, 1, 1) | |
| Preset pillglides to the preset matching the range | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Month changefrom the arrows or a preset: slides in from the side it comes from | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) |
Reduced motion
The pill jumps to its preset, the months swap in place and the popover opens and closes at once.
Reduced motion
The preset pill moves without gliding, the calendar changes month in place, and the popover appears at once. Presets, typing and announcements are unchanged.
AccessibilityARIA, focus, announcements
Accessibility
- The Field's label names the group, and the two inputs are named by it together with
startLabelandendLabel("Start date", "End date"). Each carriesaria-invalidwhen its text isn't an allowed date. - The calendar button and the popup are named "Choose dates". Presets are toggle buttons with
aria-pressed, in a group named "Presets". Focus starts on the presets, or on the calendar when there are none. - The footer ("7 days: Sep 21, 2026 – Sep 27, 2026") is announced politely as the range changes.
- With
name, hidden inputs submit${name}Fromand${name}Toasyyyy-MM-dd.