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
pnpm dlx @hex-core/cli add date-picker

Birth-date picker with year dropdown

Use captionLayout='dropdown' with explicit startMonth/endMonth to get native <select> year navigation

tsx
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.)

tsx
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

PropTypeDefaultDescription
value
objectControlled selected Date
onChange
functionCallback when the user selects a date: (date: Date | undefined) => void
placeholder
stringPick a dateText shown on the trigger when no date is selected
dateFormat
stringPPPdate-fns format token for the trigger label (e.g. 'PPP', 'yyyy-MM-dd')
disabled
booleanfalseDisable the picker trigger
aria-label
stringAccessible label — required when no adjacent visible <label> is used
captionLayout
stringCaption 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
objectEarliest month/year navigable in the calendar (Date). Forwarded to react-day-picker. Pair with captionLayout='dropdown'.
endMonth
objectLatest 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.

Related components

Token budget: 700