Date Range Picker
Date range picker field: an outline button showing the formatted range opens a popover with a two-month range Calendar. Works controlled or uncontrolled and formats dates with date-fns.
pnpm dlx shadcn add @01orbital/date-range-picker"use client"
import { useId, useState, type ComponentProps, type ReactNode } from "react"
import { CalendarBlankIcon } from "@phosphor-icons/react"
import { format } from "date-fns"
import { type DateRange } from "react-day-picker"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Field, FieldLabel } from "@/components/ui/field"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
type DateRangePickerProps = Omit<
ComponentProps<typeof Field>,
"children" | "defaultValue"
> & {
label: ReactNode
placeholder: ReactNode
// Controlled with value + onValueChange, or seeded with defaultValue.
value?: DateRange
defaultValue?: DateRange
onValueChange?: (range: DateRange | undefined) => void
// date-fns pattern for the trigger text.
dateFormat?: string
numberOfMonths?: number
defaultOpen?: boolean
}
function DateRangePicker(pickerProps: DateRangePickerProps) {
const {
label,
placeholder,
value,
defaultValue,
onValueChange,
dateFormat = "LLL dd, y",
numberOfMonths = 2,
defaultOpen = false,
className,
...props
} = pickerProps
const id = useId()
const [uncontrolled, setUncontrolled] = useState(defaultValue)
const controlled = Object.hasOwn(pickerProps, "value")
const range = controlled ? value : uncontrolled
const select = (next: DateRange | undefined) => {
if (!controlled) setUncontrolled(next)
onValueChange?.(next)
}
return (
<Field data-slot="date-range-picker" className={className} {...props}>
<FieldLabel htmlFor={id}>{label}</FieldLabel>
<Popover defaultOpen={defaultOpen}>
<PopoverTrigger
render={
<Button
variant="outline"
id={id}
className="justify-start px-2.5 font-normal"
/>
}
>
<CalendarBlankIcon data-icon="inline-start" />
{range?.from ? (
range.to ? (
<>
{format(range.from, dateFormat)} -{" "}
{format(range.to, dateFormat)}
</>
) : (
format(range.from, dateFormat)
)
) : (
<span>{placeholder}</span>
)}
</PopoverTrigger>
{/* Two months stack on a phone: the popup keeps to the room the
screen has and scrolls, so neither month runs off the top. */}
<PopoverContent
className="max-h-(--available-height) w-auto overflow-y-auto p-0"
align="start"
>
<Calendar
mode="range"
defaultMonth={range?.from}
selected={range}
onSelect={select}
numberOfMonths={numberOfMonths}
/>
</PopoverContent>
</Popover>
</Field>
)
}
export { DateRangePicker }
import { DateRangePicker } from "@/components/forms/date-range-picker"
// Fixed dates, so the server and the browser render the same range.
const STAY = { from: new Date(2026, 10, 12), to: new Date(2026, 10, 20) }
export default function DateRangePickerDemo() {
return (
<DateRangePicker
label="Stay"
placeholder="Pick your dates"
defaultValue={STAY}
className="w-full max-w-xs"
/>
)
}
Usage
import { DateRangePicker } from "@/components/forms/date-range-picker"
// Fixed dates, so the server and the browser render the same range.
const STAY = { from: new Date(2026, 10, 12), to: new Date(2026, 10, 20) }
export default function DateRangePickerDemo() {
return (
<DateRangePicker
label="Stay"
placeholder="Pick your dates"
defaultValue={STAY}
className="w-full max-w-xs"
/>
)
}Examples
New
date-range-picker-cabin-booking
API reference
Prop
Type
It also accepts the HTML attributes of the element it renders.
Dependencies
npm: @phosphor-icons/react, date-fns, react-day-picker. The CLI installs them for you.
Registry pieces it installs with it: