Skip to main content

DatePicker

Single-date selection with an input, trigger, popover, and calendar grid.

import { DatePicker } from '@kalyx/react';

Basic usage

import { useState } from 'react';
import { DatePicker, type ISODateString } from '@kalyx/react';

function Example() {
const [date, setDate] = useState<ISODateString | null>(null);
return (
<DatePicker value={date} onChange={setDate}>
<DatePicker.Input placeholder="YYYY-MM-DD" />
<DatePicker.Trigger />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
);
}

Try it live

Live Editor
function BasicDatePicker() {
  const [date, setDate] = React.useState(null);
  const [view, setView] = React.useState('days');
  const headerCls = {
    header: 'kx-live-header',
    title: 'kx-live-title',
    navButton: 'kx-live-nav',
  };
  return (
    <DatePicker value={date} onChange={setDate}>
      <div className="kx-live-row">
        <DatePicker.Input className="kx-live-input" placeholder="YYYY-MM-DD" />
        <DatePicker.Trigger className="kx-live-trigger" aria-label="Open calendar" />
      </div>
      <DatePicker.Popover className="kx-live-popover">
        {view === 'days' && (
          <DatePicker.Calendar
            onTitleClick={() => setView('months')}
            classNames={{
              ...headerCls,
              grid: 'kx-live-grid',
              gridCell: 'kx-live-cell',
              weekdayHeader: 'kx-live-weekday',
              day: 'live-day',
              daySelected: 'live-day-selected',
              dayToday: 'live-day-today',
              dayDisabled: 'kx-live-disabled',
              dayOutsideMonth: 'kx-live-outside',
            }}
          />
        )}
        {view === 'months' && (
          <DatePicker.MonthGrid
            onSelect={() => setView('days')}
            onTitleClick={() => setView('years')}
            classNames={{
              ...headerCls,
              grid: 'kx-live-month-grid',
              month: 'kx-live-my-cell',
              monthSelected: 'kx-live-my-selected',
              monthCurrent: 'kx-live-my-current',
            }}
          />
        )}
        {view === 'years' && (
          <DatePicker.YearGrid
            onSelect={() => setView('months')}
            classNames={{
              ...headerCls,
              grid: 'kx-live-year-grid',
              year: 'kx-live-my-cell',
              yearSelected: 'kx-live-my-selected',
              yearCurrent: 'kx-live-my-current',
            }}
          />
        )}
      </DatePicker.Popover>
      <div className="kx-live-value">
        Selected: <code>{date ?? 'null'}</code> — click the month title to jump to Month / Year view.
      </div>
    </DatePicker>
  );
}
Result
Loading...

<DatePicker> (Root)

Holds state and provides context to sub-components. Controlled when value is provided; uncontrolled when only defaultValue is.

PropTypeDefaultDescription
valueISODateString | nullControlled selected date.
defaultValueISODateStringUncontrolled initial value. Ignored if value is set.
onChange(value: ISODateString | null) => voidFires when a date is selected or cleared.
disabledDisabledRule[] | booleanfalseDisable specific dates, or disable the whole picker.
readOnlybooleanfalsePrevents changes; still selectable visually for form display.
weekStartsOn0 | 100 = Sunday, 1 = Monday.
displayFormatstring'yyyy-MM-dd'date-fns format string.
localestring'en-US'BCP 47 locale tag.
displayTimezonestringIANA zone (e.g., "Asia/Seoul"). When set, Input formats in this zone, Calendar highlights match civil days, and onChange emits civil midnight in this zone. See Timezone.
adapterDateAdapterDateFnsAdapterCustom date adapter.
labelsPartial<DatePickerLabels>Override ARIA labels (defaults to English). Keys: triggerOpen, triggerClose, popoverLabel, prevMonth, nextMonth, prevYear, nextYear, prevDecade, nextDecade.
childrenReactNodeSub-components.

DisabledRule

type DisabledRule =
| { date: ISODateString } // exact day
| { before: ISODateString } // any day strictly before
| { after: ISODateString } // any day strictly after
| { dayOfWeek: number[] }; // 0 = Sun … 6 = Sat

Disable all weekends and any date before today:

<DatePicker
value={iso}
onChange={setIso}
disabled={[{ dayOfWeek: [0, 6] }, { before: new Date().toISOString() }]}
/>
Live Editor
function WeekdayOnly() {
  const [date, setDate] = React.useState(null);
  const today = new Date().toISOString();
  return (
    <DatePicker
      value={date}
      onChange={setDate}
      disabled={[{ dayOfWeek: [0, 6] }, { before: today }]}
    >
      <DatePicker.Input className="kx-live-input" placeholder="Weekdays only, from today" />
      <DatePicker.Popover className="kx-live-popover">
        <DatePicker.Calendar
          classNames={{
            header: 'kx-live-header',
            title: 'kx-live-title',
            navButton: 'kx-live-nav',
            grid: 'kx-live-grid',
            gridCell: 'kx-live-cell',
            weekdayHeader: 'kx-live-weekday',
            day: 'live-day',
            daySelected: 'live-day-selected',
            dayToday: 'live-day-today',
            dayDisabled: 'kx-live-disabled',
            dayOutsideMonth: 'kx-live-outside',
          }}
        />
      </DatePicker.Popover>
    </DatePicker>
  );
}
Result
Loading...

<DatePicker.Input>

Renders an <input role="combobox">. Parses typed dates on blur / Enter. Extends all standard input attributes (except value, onChange, type).

PropTypeDescription
formatstringOverrides the Root's displayFormat for parsing / display.

Supports ref forwarding to the underlying <input>.

<DatePicker.Trigger>

A button that toggles the popover. Renders a default calendar icon if no children are provided. Extends all standard button attributes (except type).

PropTypeDescription
childrenReactNodeOverride the default icon.
<DatePicker.Trigger aria-label="Open calendar">
📅
</DatePicker.Trigger>

<DatePicker.Popover>

Floating-UI-positioned portal (role="dialog", aria-modal="false"). Handles outside-click dismissal, Escape-to-close, and focus restoration.

Extends standard <div> attributes (except role).

<DatePicker.Popover className="rounded-lg border bg-white p-3 shadow-lg">
<DatePicker.Calendar />
</DatePicker.Popover>

<DatePicker.Calendar>

Renders the month grid. Fully keyboard navigable (see Accessibility).

PropTypeDescription
classNamesDatePickerCalendarClassNamesStyling for internal slots.
onTitleClick() => voidFires when the month/year title is clicked — wire to MonthGrid / YearGrid.

classNames keys

type DatePickerCalendarClassNames = {
root?: string;
header?: string; // wraps title + nav buttons
title?: string; // "April 2026"
navButton?: string; // prev / next buttons
grid?: string; // <table role="grid">
gridRow?: string; // <tr>
gridCell?: string; // <td role="gridcell">
day?: string; // each day button
daySelected?: string; // applied when day.isSelected
dayToday?: string; // applied when day.isToday
dayDisabled?: string; // applied when day.isDisabled
dayOutsideMonth?: string;// days padding the 6-week view
weekdayHeader?: string; // "Mon", "Tue", …
};

<DatePicker.MonthGrid> (optional)

A 3×4 grid of months — drop in to let users jump directly to a month.

PropTypeDescription
classNamesDatePickerMonthGridClassNamesStyling.
onSelect() => voidFired after picking a month — typically switches back to Calendar.
onTitleClick() => voidFired when the year title is clicked — wire to YearGrid.
type DatePickerMonthGridClassNames = {
root?: string;
header?: string;
title?: string;
navButton?: string;
grid?: string;
month?: string;
monthSelected?: string;
monthCurrent?: string;
};

<DatePicker.YearGrid> (optional)

A paginated grid of years (12 per page).

PropTypeDescription
classNamesDatePickerYearGridClassNamesStyling.
onSelect() => voidFired after picking a year.
type DatePickerYearGridClassNames = {
root?: string;
header?: string;
title?: string;
navButton?: string;
grid?: string;
year?: string;
yearSelected?: string;
yearCurrent?: string;
};

<DatePicker.Presets> / <DatePicker.Preset> (optional)

Quick-select buttons for common dates. <DatePicker.Presets> is a role="group" container; each <DatePicker.Preset> is a regular toggle button (using aria-pressed) that commits and closes the popover on click.

<DatePicker value={date} onChange={setDate}>
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Presets>
<DatePicker.Preset value="today">Today</DatePicker.Preset>
<DatePicker.Preset value="tomorrow">Tomorrow</DatePicker.Preset>
<DatePicker.Preset value="startOfMonth">Start of month</DatePicker.Preset>
<DatePicker.Preset date="2026-12-25T00:00:00.000Z">Christmas</DatePicker.Preset>
</DatePicker.Presets>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>

Preset keys: today, tomorrow, yesterday, startOfMonth, endOfMonth, startOfYear. Or pass a concrete date (ISO 8601 UTC) for anything else. Active presets get aria-pressed="true" and data-active when the resolved date matches the current value (timezone-aware via displayTimezone).

Event callbacks

All DatePicker.Root callbacks are optional. Neither of the two newer ones fires on initial mount.

PropSignatureFires when
onChange(value: ISODateString | null) => voidA date is committed (click / Enter / preset / input typed).
onOpenChange(isOpen: boolean) => voidThe popover opens or closes (any reason — click, keyboard, outside click, selection).
onCalendarNavigate(viewMonth: ISODateString) => voidThe calendar view moves to a different month. Emits the first day of the newly-visible month in UTC.
<DatePicker
value={date}
onChange={setDate}
onOpenChange={(open) => analytics.track('picker_toggle', { open })}
onCalendarNavigate={(month) => prefetchEventsForMonth(month)}
>
{/* ... */}
</DatePicker>

Patterns

Month / Year navigation

import { useState } from 'react';
import { DatePicker } from '@kalyx/react';

function WithJump() {
const [view, setView] = useState<'days' | 'months' | 'years'>('days');
return (
<DatePicker value={iso} onChange={setIso}>
<DatePicker.Input />
<DatePicker.Popover>
{view === 'days' && (
<DatePicker.Calendar onTitleClick={() => setView('months')} />
)}
{view === 'months' && (
<DatePicker.MonthGrid
onSelect={() => setView('days')}
onTitleClick={() => setView('years')}
/>
)}
{view === 'years' && (
<DatePicker.YearGrid onSelect={() => setView('months')} />
)}
</DatePicker.Popover>
</DatePicker>
);
}

The “Try it live” example at the top of this page already wires this flow — tap the month title to open MonthGrid, tap the year to open YearGrid.

Uncontrolled with form submission

<form action="/api/save" method="post">
<DatePicker name="startDate" defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input name="startDate" />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
<button type="submit">Save</button>
</form>

Min / max dates

There's no minDate / maxDate prop — express the same rule with disabled:

<DatePicker
disabled={[
{ before: '2026-01-01T00:00:00.000Z' },
{ after: '2026-12-31T00:00:00.000Z' },
]}
value={iso}
onChange={setIso}
/>