Date Picker
Date input composed from Popover + Calendar. Shows the selected date formatted via date-fns, opens a calendar grid on click.
Empty
Pre-selected
Disabled
Installation
pnpm dlx @hex-core/cli add date-pickerBirth-date picker with year dropdown
Use captionLayout='dropdown' with explicit startMonth/endMonth to get native <select> year navigation
import { useState } from "react";
import { DatePicker } from "@/components/ui/date-picker";
export function Example() {
const [dob, setDob] = useState<Date | undefined>();
return (
<DatePicker
value={dob}
onChange={setDob}
placeholder="Date of birth"
captionLayout="dropdown"
startMonth={new Date(1925, 0)}
endMonth={new Date(new Date().getFullYear(), 11)}
/>
);
}With min/max month constraints
Pin startMonth/endMonth to constrain the navigable window — for an appointment booker, restrict to the next 90 days so users can't page back into the past or jump six months ahead. (DatePicker exposes month-level bounds, not per-day matchers; for day-granular blocking, compose Popover + Calendar with disabled={...} directly.)
import { useMemo, useState } from "react";
import { DatePicker } from "@/components/ui/date-picker";
export function Example() {
const [appointment, setAppointment] = useState<Date | undefined>();
const { startMonth, endMonth } = useMemo(() => {
const today = new Date();
const end = new Date(today);
end.setDate(today.getDate() + 90);
return {
startMonth: new Date(today.getFullYear(), today.getMonth(), 1),
endMonth: new Date(end.getFullYear(), end.getMonth(), 1),
};
}, []);
return (
<DatePicker
value={appointment}
onChange={setAppointment}
placeholder="Pick an appointment"
startMonth={startMonth}
endMonth={endMonth}
/>
);
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
value | object | — | Controlled selected Date |
onChange | function | — | Callback when the user selects a date: (date: Date | undefined) => void |
placeholder | string | Pick a date | Text shown on the trigger when no date is selected |
dateFormat | string | PPP | date-fns format token for the trigger label (e.g. 'PPP', 'yyyy-MM-dd') |
disabled | boolean | false | Disable the picker trigger |
aria-label | string | — | Accessible label — required when no adjacent visible <label> is used |
captionLayout | string | — | Caption layout forwarded to react-day-picker. Use 'dropdown' (or 'dropdown-years' / 'dropdown-months') for native <select> navigation — common for birth-date pickers. Default is 'label' (chevron buttons only). |
startMonth | object | — | Earliest month/year navigable in the calendar (Date). Forwarded to react-day-picker. Pair with captionLayout='dropdown'. |
endMonth | object | — | Latest month/year navigable in the calendar (Date). Forwarded to react-day-picker. Pair with captionLayout='dropdown'. |
AI Guidance
When to use
Use for selecting a single date in a form. Shows a formatted text label and opens a month grid on click. Composes Popover + Calendar + button trigger. For far-away years (birthdays, historical dates), pass captionLayout='dropdown' plus startMonth/endMonth.
When not to use
Don't use for date ranges (compose Calendar mode='range' + Popover yourself). Don't use for native mobile date UX (<input type='date'> is often better on phones). Don't use if you need time selection — combine with a separate time picker.
Common mistakes
- Passing a string to value — must be a Date object
- Missing aria-label when the picker has no adjacent visible <label>
- Overriding className in a way that hurts focus ring visibility
- Forgetting that the popover auto-closes on select — provide onChange to capture the value
- Setting captionLayout='dropdown' without startMonth/endMonth — react-day-picker defaults to ±100 years, producing a 200-option dropdown
Accessibility
Trigger is a real <button> with focus ring. When rendered without a visible label, pass aria-label. The popover portals and traps keyboard focus inside Calendar until the user selects or presses Escape.
Token budget: 700
Verified against @hex-core/components@1.12.0