cupertino-datetime-picker

CalendarPanel and TimePanel

The two panels on their own, for your own popovers, sheets and rows.

Both panels are plain controlled components: a value, an onChange, and a locale. DateTimePicker is a thin layer over them that adds the pills and popovers; anything it does, you can do with the panels directly.

import { CalendarPanel } from "@/components/ui/cupertino/calendar-panel";
import { TimePanel } from "@/components/ui/cupertino/time-panel";

<CalendarPanel value={date} onChange={setDate} />
<TimePanel value={date ?? new Date()} onChange={setDate} />

Neither panel draws a background. Give the container the cdp class (font and label colour), bg-[var(--cdp-bg)], and the corners and shadow you want.

CalendarPanel

The iOS inline calendar. The month title opens month and year wheels over the grid; next and previous slide the month in; a horizontal swipe changes month; the keyboard walks the grid. The panel is 312px wide.

PropTypeDefault
valueDate | nullThe selected day. null selects nothing.
onChange(date: Date) => voidCalled with the picked day at the value's clock time.
localestringnavigator.languageMonth and weekday names, first day of the week.
min / maxDateDays outside the range are disabled.
labelsPartial<CalendarPanelLabels>EnglishAccessible names; see Labels.
todayDatenew Date()Injected for tests and stories.
classNamestringOn the root element.

A value set from outside moves the view to its month. The grid is a role="grid" of gridcell buttons named with the full date, so a screen reader hears "Monday, 7 September 2026".

TimePanel

Time entry the way iOS 14 does it, both halves at once: a field of hour and minute segments above hour, minute and AM/PM wheels. Type 930 and the wheels spin there; fling a wheel and the digits follow. value is never null; hand it a starting Date when there is no time yet.

PropTypeDefault
valueDateThe time shown. The day part is kept as is.
onChange(date: Date) => voidCalled with the same day at the new clock time.
localestringnavigator.languageClock and AM/PM labels.
hourCycle"h12" | "h23"from locale12- or 24-hour clock.
minuteIntervalnumber1Minute wheel step.
wheelsbooleantruefalse keeps only the typed field.
labelsPartial<TimePanelLabels>EnglishAccessible names; see Labels.
classNamestringOn the root element.
<TimePanel value={date} onChange={setDate} wheels={false} />

Labels

Visible text comes from Intl, so a locale change translates the panels by itself. The accessible names (button labels, wheel names) are English by default and are the one thing to translate:

<CalendarPanel
  labels={{ previousMonth: "Vormonat", nextMonth: "Nächster Monat", month: "Monat", year: "Jahr" }}
/>
<TimePanel labels={{ time: "Uhrzeit", hour: "Stunde", minute: "Minute", dayPeriod: "Tageshälfte" }} />

CALENDAR_LABELS and TIME_LABELS export the defaults, and CalendarPanelLabels / TimePanelLabels are their types.

A row of your own

pillClass and popupClass are the compact picker's pill and popover styles. With them and a Base UI popover, a row that adds a time only when asked looks like the picker's own:

import { Popover } from "@base-ui/react/popover";
import { pillClass, popupClass } from "@/components/ui/cupertino/date-time-picker";
import { formatClock, hourCycleFor } from "@/components/ui/cupertino/time";
import { TimePanel } from "@/components/ui/cupertino/time-panel";

function TimeRow({ value, onChange, locale }) {
  const hourCycle = hourCycleFor(locale);
  return (
    <div className="cdp mx-3 flex h-[52px] items-center justify-between">
      <span className="text-[17px]">Time</span>
      <Popover.Root>
        <Popover.Trigger className={pillClass}>
          {value ? formatClock(value, locale, hourCycle) : "Add time"}
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Positioner sideOffset={8} align="end">
            <Popover.Popup className={popupClass}>
              <TimePanel value={value ?? new Date()} onChange={onChange} locale={locale} />
            </Popover.Popup>
          </Popover.Positioner>
        </Popover.Portal>
      </Popover.Root>
    </div>
  );
}

On this page