A segmented date field with a calendar popover, for single dates, dates with a time, or date ranges. Users can type or pick; both stay in sync, with min/max bounds, unavailable dates, validation and native form submission built in.
value is a CalendarDate (or CalendarDateTime / ZonedDateTime when you
need a time), and ranges are { start, end }. Create values with today(),
parseDate() or parseZonedDateTime() from
@internationalized/date,
and store them with value.toString().
Dates outside minValue / maxValue are disabled in the calendar, and typed values outside them are flagged invalid with a localized message. Here validationBehavior="aria" shows the error straight away; with the default native behavior it appears after the value changes or the form is submitted.
Start date
10/3/2026
Within the next three months.Value must be 10/5/2026 or later.
import { getLocalTimeZone, today } from "@internationalized/date";import { DatePicker } from "@/components/ui/date-picker";export default function DatePickerValidation() { const now = today(getLocalTimeZone()); return ( <DatePicker label="Start date" validationBehavior="aria" minValue={now} maxValue={now.add({ months: 3 })} defaultValue={now.subtract({ days: 2 })} description="Within the next three months." className="w-full max-w-60" /> );}
captionLayout, fromYear and toYear are passed to the calendar in the popover (see Calendar). Use captionLayout="dropdown" for dates years away from today, like a date of birth, so nobody has to click back month by month. placeholderValue sets the month the calendar opens on.
Date of birth
mm/dd/yyyy
Pick a month and year from the calendar header.
import { type CalendarDate, getLocalTimeZone, today,} from "@internationalized/date";import { useState } from "react";import { DatePicker } from "@/components/ui/date-picker";export default function DatePickerBirthday() { const now = today(getLocalTimeZone()); const [value, setValue] = useState<CalendarDate | null>(null); const age = value ? now.year - value.year - (now.compare(value.set({ year: now.year })) < 0 ? 1 : 0) : null; return ( <div className="flex w-full max-w-xs flex-col gap-3"> <DatePicker label="Date of birth" description="Pick a month and year from the calendar header." captionLayout="dropdown" fromYear={1940} maxValue={now} placeholderValue={now.set({ year: 1990, month: 1, day: 1 })} value={value} onChange={setValue} /> {age != null && ( <p className="text-muted-foreground text-sm"> Age: <span className="font-medium text-foreground">{age}</span> </p> )} </div> );}
The range picker takes the same props. Here the year list is limited to this year and next by minValue and maxValue.
Check-in and check-out
mm/dd/yyyy
→
mm/dd/yyyy
Reservations open up to a year ahead.
import { getLocalTimeZone, today } from "@internationalized/date";import { DateRangePicker } from "@/components/ui/date-picker";export default function DatePickerBooking() { const now = today(getLocalTimeZone()); return ( <DateRangePicker label="Check-in and check-out" description="Reservations open up to a year ahead." captionLayout="dropdown" minValue={now} maxValue={now.add({ years: 1 })} className="w-full max-w-xs" /> );}
With granularity="minute" (or hour / second) the field adds time segments. The calendar picks the date and keeps the time. Use a ZonedDateTime for a specific time zone, hideTimeZone to hide its abbreviation, and hourCycle to force a 12- or 24-hour clock.
Format, segment order and the calendar's first weekday follow the locale. Wrap pickers in I18nProvider to set one, or override the week start with firstDayOfWeek.
isOpen and onOpenChange control the popover, for example to open it from another button. shouldCloseOnSelect={false} keeps it open after a date is picked.
Follow-up
mm/dd/yyyy
Stays open after picking. Press Esc or click outside to close.
import { useState } from "react";import { Button } from "@/components/ui/button";import { DatePicker } from "@/components/ui/date-picker";export default function DatePickerControlledOpen() { const [open, setOpen] = useState(false); return ( <div className="flex w-full max-w-60 flex-col gap-3"> <DatePicker label="Follow-up" isOpen={open} onOpenChange={setOpen} shouldCloseOnSelect={false} description="Stays open after picking. Press Esc or click outside to close." /> <Button size="sm" variant="outline" className="self-start" onPress={() => setOpen(true)} > Open calendar </Button> </div> );}
A preset Select next to a DateRangePicker. Choosing a preset fills the range; editing the range switches the preset to "Custom". maxValue blocks future dates.
Period
9/6/2026
→
10/5/2026
import { type CalendarDate, getLocalTimeZone, startOfMonth, startOfYear, today,} from "@internationalized/date";import { CalendarRangeIcon } from "lucide-react";import { useState } from "react";import type { Key, RangeValue } from "react-aria-components";import { DateRangePicker } from "@/components/ui/date-picker";import { Select, SelectItem } from "@/components/ui/select";type Range = RangeValue<CalendarDate>;function presetRange(id: Key, now: CalendarDate): Range | null { switch (id) { case "7d": return { start: now.subtract({ days: 6 }), end: now }; case "30d": return { start: now.subtract({ days: 29 }), end: now }; case "90d": return { start: now.subtract({ days: 89 }), end: now }; case "mtd": return { start: startOfMonth(now), end: now }; case "ytd": return { start: startOfYear(now), end: now }; default: return null; }}export default function DatePickerRecipeReportRange() { const now = today(getLocalTimeZone()); const [preset, setPreset] = useState<Key | null>("30d"); const [range, setRange] = useState<Range | null>(presetRange("30d", now)); return ( <div className="flex w-full max-w-xl flex-wrap items-end gap-2 rounded-lg border bg-card p-3 shadow-xs"> <Select label="Period" size="sm" prefix={<CalendarRangeIcon />} selectedKey={preset} onSelectionChange={(key) => { setPreset(key); if (key && key !== "custom") setRange(presetRange(key, now)); }} className="w-44" > <SelectItem id="7d">Last 7 days</SelectItem> <SelectItem id="30d">Last 30 days</SelectItem> <SelectItem id="90d">Last 90 days</SelectItem> <SelectItem id="mtd">Month to date</SelectItem> <SelectItem id="ytd">Year to date</SelectItem> <SelectItem id="custom">Custom</SelectItem> </Select> <DateRangePicker aria-label="Custom range" size="sm" value={range} onChange={(value) => { setRange(value); setPreset("custom"); }} maxValue={now} className="w-auto min-w-64 flex-1" /> </div> );}
A native <form> with a required range (sold-out nights unavailable, the night count in the description), startName / endName for submission, and a NumberField for guests.
className sizes and positions the whole picker (e.g. w-full max-w-60). It also accepts a function of the render props.
The field chrome uses the shared fieldVariants, so pickers line up with text fields and selects of the same size.
Segments use the exported dateSegmentStyles from Date Field.
The popover and calendar are fixed parts. For a different popover layout (presets beside the calendar, two months), compose React Aria's DatePicker with this library's Popover and Calendar.