Date Range Picker React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React date range picker for reports and bookings: two months with presets on desktop, a one-month bottom sheet on phones.
Click the field to open a panel under it: presets on the left (Last 7 days, This month, Last quarter...), two months on the right and a summary with Cancel and Apply at the bottom. The first click on a day sets the start, the band follows the pointer as a preview, and the second click sets the end (a day before the start swaps them); days that would break minDays, maxDays or cross a disabled day are unavailable while you pick the end. A preset fills both ends and moves the calendar to it. The summary under the calendar updates as you go (the dates, then "· 7 days"), and Apply commits; Cancel, Escape or a click outside throw the draft away, so a half-picked range never reaches the value. With confirm={false} the range commits as soon as both ends are picked, or on a preset. The clear button in the field empties it. Keyboard: Enter, Space or ArrowDown on the field open the panel and move focus into the calendar (the start of the range, else today); arrows move a day or a week, Home / End jump to the week's ends, PageUp / PageDown move a month (Shift: a year), Enter picks the start and then the end; Tab reaches Cancel and Apply, Shift+Tab the presets (arrow keys move between presets); Ctrl+Enter (Cmd+Enter on a Mac) applies from anywhere in the panel; Escape cancels and returns focus to the field. On a phone the field opens a bottom sheet with the presets as a row of chips, two Start / End cells (the end being picked is outlined), one month you can swipe, and Apply / Cancel pinned above the home indicator; dragging the sheet down cancels.
Date Range Picker is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add date-range-picker, together with Button, Calendar, Drawer, Field, Date utils and 6 more and the npm packages @radix-ui/react-popover, date-fns, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add date-range-pickerDate Range Picker is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import DateRangePicker from @/components/ui/date-range-picker.
import { useState } from "react"
import { DateRangePicker, type DateRange } from "@/components/ui/date-range-picker"
export function SalesReport() {
const [range, setRange] = useState<DateRange>({ from: null, to: null })
return (
<DateRangePicker
label="Period"
name="period"
locale="en-US"
placeholder="Pick a period"
max={new Date()}
value={range}
onValueChange={setRange}
presetLabels={{ lastQuarter: "Previous quarter" }}
labels={{ duration: (days) => (days === 1 ? "1 day" : `${days} days`) }}
/>
)
}Props
labeldefault""ReactNodeRenders the field anatomy around the trigger. A string label also titles the phone sheet and names the panel. A click on the label focuses the trigger without opening it.
descriptiondefault""ReactNodeHelper text under the field, linked with aria-describedby. The error takes its place while it shows.
errordefault""ReactNodeReplaces the description with the animated error and marks the field invalid (red ring, aria-invalid on the trigger).
requireddefaultfalsebooleanThe mark after the label and aria-required on the trigger.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
disableddefaultfalseboolean or DateMatcher or DateMatcher[] (DateMatcher: Date, Date[], { from, to }, { before }, { after }, { dayOfWeek } or (date) => boolean)true dims the field, blocks it and leaves the hidden inputs out of the submit. A DateMatcher instead disables those days in the calendar: a Date, a list, { from, to }, { before }, { after }, { dayOfWeek: [0, 6] } or a (date) => boolean test (booked nights, holidays).
readOnlydefaultfalsebooleanThe value stays visible and the trigger focusable, but it never opens and the clear button hides.
sizedefault"md""sm" | "md" | "lg"The field box: 32 / 40 / 48px (md is 44px on touch screens), like every other field.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The box look, shared with every field.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"The box corners, per size like the button (8 / 10 / 12px at md). Ignored by underline.
placeholderdefault"Select dates"stringThe trigger text while no range is set.
localedefault""stringMonth and weekday names, digits, the first day of the week and the trigger text. Empty: the nearest LocaleProvider, else the browser.
displayFormatIntl.DateTimeFormatOptionsHow the trigger and the summary write the range, through Intl formatRange: { day: "numeric", month: "short", year: "numeric" } gives "1 to 30 sept. 2026" joined by an en dash; only a start gives the start followed by a dash.
numberOfMonthsdefault21 | 2 | 3Months side by side on desktop. The panel drops months that do not fit the screen; phones always show one.
presetsdefaulttrueboolean | (RangePresetKey | DateRangePreset)[]true: Last 7 days, Last 30 days, This month, Last month, This quarter, Last quarter, This year. Or a list mixing built-in keys ("today", "yesterday", "last90Days", "thisWeek", "lastWeek", "lastYear", "yearToDate"...) and your own { label, value: DateRange | () => DateRange }. Presets that run past min / max are cut to them; fully outside, or longer than maxDays, they are disabled.
presetLabelsPartial<Record<RangePresetKey, string>>Labels for the built-in presets (English by default).
minDateEarlier days are disabled and the calendar stops at this month.
maxDateLater days are disabled (reports pass today) and the calendar stops at this month.
minDaysdefault0numberThe shortest range in days, both ends counted (a 2 night stay is 3 days). Closer ends are unavailable while picking.
maxDaysdefault0numberThe longest range in days, both ends counted. Further ends are unavailable while picking, longer presets are disabled.
excludeDisableddefaulttruebooleanA range can not span a disabled day: past the first one, the days are unavailable while picking the end (bookings).
confirmdefaulttruebooleanApply / Cancel buttons; the value only changes on Apply. false commits and closes as soon as both ends are picked, or on a preset.
clearabledefaulttruebooleanA clear button in the field while a range is set.
showDurationdefaulttruebooleanThe summary shows the length ("7 days", or nights with labels.nights).
labels{ apply?; cancel?; clear?; start?; end?; pickStart?; pickEnd?; presets?; duration?: (days) => string; nights?: (nights) => string }Every string of the panel: { apply: "Apply", cancel: "Cancel", clear: "Clear", start: "Start", end: "End", pickStart: "Pick a start date", pickEnd: "Pick an end date", presets: "Presets", duration: (d) => d + " days" }. nights switches the summary to nights (a 4 day stay is 3 nights).
weekStartsOndefault-10 | 1 | 2 | 3 | 4 | 5 | 6 | -10 Sunday ... 6 Saturday; -1 follows the locale (Monday in ro-RO, Sunday in en-US).
eventsCalendarEvent[]Dots under days in the calendar: { date, tone?, color?, label? }.
calendarPropsPartial<CalendarProps<"range">>Anything else for the calendar: size, radius, strikeDisabled, showWeekNumbers, renderDay, labels (Romanian month navigation), fixedWeeks (on by default so the panel never changes height).
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The color of the picked ends, the band, the active preset check, the outline of the cell being picked and the Apply button.
colordefault""stringAny CSS color; replaces the tone. Text on the fill turns black or white by lightness.
iconReactNodeThe leading icon of the field (CalendarRange by default).
defaultOpendefaultfalsebooleanStarts open while uncontrolled.
valueDateRangeThe committed range, controlled: { from: Date | null; to: Date | null }. A value with only from shows the start followed by a dash; a backwards one is shown in order.
defaultValueDateRangeThe starting range while uncontrolled; { from: null, to: null } by default. A form reset returns to it.
onValueChange(range: DateRange) => voidCalled on Apply, on a clear, or as soon as both ends are picked with confirm={false}. Never with a half-picked draft.
openbooleanThe panel or sheet, controlled. Pair it with onOpenChange. Every opening starts from the committed value.
onOpenChange(open: boolean) => voidCalled when the panel opens or closes.
responsivedefaulttruebooleanA bottom sheet below 768px instead of the popover.
sheetTitledefault"Select dates"stringThe sheet title and the panel name when the label is not plain text.
sidedefault"bottom""top" | "right" | "bottom" | "left"Where the panel opens; it flips when there is no room.
aligndefault"start""start" | "center" | "end"The panel's alignment against the field.
containerHTMLElement | nullPortal target for the panel and the sheet (document.body by default).
namedefault""stringRenders hidden inputs name.from and name.to with ISO dates ("2026-09-23", local, never UTC).
idstringThe trigger's id (generated by default); the label points at it.
fieldPropsPartial<FieldProps>Anything else for the field wrapper: optional, labelAction, orientation, labelWidth, hideLabel, className.
classNamesPartial<Record<DateRangePickerSlot, string>>Classes for the parts: field, label, description, error, counter, box, trigger, icon, value, clear, chevron, content, presets, preset, calendar, footer, summary, cell (the phone Start / End cells), actions. className goes on the field root (or the box without a label).
refRefObject<HTMLButtonElement | null> or (node) => voidThe trigger button; the rest of the props (onBlur, aria-*, data-*) land on it too, so react-hook-form can focus and touch it.
Parts
DateRangePreset
typeOne preset button: { label, value: DateRange | (() => DateRange), key? }.
Examples
Booking a stay
Booked nights are disabled and struck through, minDays 3 (2 nights), labels.nights counts nights and the price follows.
Instant filter
confirm={false}: the list filters as soon as both ends are picked, or on a preset tap. Mixed built-in preset keys.
Compact
numberOfMonths 1 with a small outline field and small calendar cells, for a sidebar or a toolbar.
Custom presets
Your own ranges, computed when the panel opens: this weekend, next week, the next 30 days. Brand tone for the selection.
Maximum length
maxDays 31: ends further out are unavailable while picking, and presets longer than that are disabled.
Locales
The same labels in two locales: ro-RO starts weeks on Monday and writes the day before the month; en-US starts on Sunday and writes the month first.
Sizes and variants
sm soft, md outline, lg underline, a ghost field and a disabled one: the same box as every other field.
Dependencies
Files
- components/ui/date-range-picker.tsx
FAQ
Is the React Date Range Picker component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Date Range Picker?
Run npx wingo-ui@latest add date-range-picker. It also adds the Wingo UI items it builds on (Button, Calendar, Drawer, Field, Date utils and 6 more) and the npm packages @radix-ui/react-popover, date-fns, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/date-range-picker.
How does Date Range Picker behave on phones?
A bottom sheet below 768px instead of the popover. The responsive prop controls this and is on by default.
See also
Used in
More React input components
Articles using this component
- React Month Picker: Months, Quarters and Ranges
Component guides8 min read
- React Date Range Picker With Presets and a Phone Sheet
Component guides9 min read