Skip to content

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-picker

For 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.

Reference

Propsvalue, defaultValue, onValueChange… (18)

Props

DateRangePicker props
PropType
valueDateRange | null–
defaultValueDateRange | null

default null

null
onValueChange(range: DateRange | null) => void–
presetsfalse | DateRangePreset[]

Default: Today, Yesterday, Last 7 / 30 days, This / Last month, Year to date. false hides them.

–
localeLocale

A date-fns / react-day-picker locale. Default: YoujProvider's locale, through Intl (see Calendar's dateLocale).

–
minDateDate–
maxDateDate–
disabledDatesMatcher | Matcher[]

Days that can't be chosen (react-day-picker matchers); a range can't span them.

–
numberOfMonthsnumber

Months side by side. Default 2 (1 on phones).

–
disabledboolean–
namestring

Also submits ${name}From / ${name}To (yyyy-MM-dd) with a native form.

–
idstring–
refRef<HTMLInputElement>–
onBlur() => void–
classNamestring–
startLabelstring

Names the start and end fields. Default "Start date" / "End date".

–
endLabelstring–
presentationOverlayPresentation
'auto''sheet''popover'

auto (the default) opens the calendar in a popover under a pointer and in a bottom sheet on touch; force either one.

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

Motion tokens
MomentTokens
Openthe popover grows out of the field (yj-pop)
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Close
duration.instantease.exit

80ms · cubic-bezier(0.4, 0, 1, 1)

80mscubic-bezier(0.4, 0, 1, 1)
Preset pillglides to the preset matching the range
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Month changefrom the arrows or a preset: slides in from the side it comes from
duration.baseease.enter

220ms · 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 startLabel and endLabel ("Start date", "End date"). Each carries aria-invalid when 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}From and ${name}To as yyyy-MM-dd.