MonthPicker
Month selector. The value is the first day of the selected month in UTC-ISO form — for example, picking April 2026 yields "2026-04-01T00:00:00.000Z".

import { MonthPicker } from '@kalyx/react';
Anatomy
<MonthPicker> {/* Root — value = first day of month, UTC */}
<MonthPicker.Input /> {/* combobox <input>, parses "YYYY-MM" */}
<MonthPicker.Trigger /> {/* button that toggles the popover */}
<MonthPicker.Popover> {/* Floating-UI portal, role="dialog" */}
<MonthPicker.Grid /> {/* 3×4 grid of months, role="grid" */}
</MonthPicker.Popover>
</MonthPicker>
Input and Trigger are re-exported from DatePicker and read the MonthPicker context.
Basic usage
import { useState } from 'react';
import { MonthPicker, type ISODateString } from '@kalyx/react';
function Example() {
const [month, setMonth] = useState<ISODateString | null>(null);
return (
<MonthPicker value={month} onChange={setMonth}>
<MonthPicker.Input placeholder="YYYY-MM" />
<MonthPicker.Popover>
<MonthPicker.Grid />
</MonthPicker.Popover>
</MonthPicker>
);
}
The default displayFormat is "yyyy-MM". Override it if you prefer a different representation (e.g., "MMMM yyyy" for "April 2026").
Try it live
The live editor runs with
Reactand all Kalyx components in scope, soimportlines are omitted. Copy them in when porting to your project — see the full imports in the non-live blocks above.
function BasicMonthPicker() { const [month, setMonth] = React.useState(null); const headerCls = { header: 'kx-live-header', title: 'kx-live-title', navButton: 'kx-live-nav', }; return ( <MonthPicker value={month} onChange={setMonth}> <div className="kx-live-row"> <MonthPicker.Input className="kx-live-input" placeholder="YYYY-MM" /> <MonthPicker.Trigger className="kx-live-trigger" aria-label="Open month picker" /> </div> <MonthPicker.Popover className="kx-live-popover"> <MonthPicker.Grid classNames={{ ...headerCls, grid: 'kx-live-month-grid', month: 'kx-live-my-cell', monthSelected: 'kx-live-my-selected', monthCurrent: 'kx-live-my-current', }} /> </MonthPicker.Popover> <div className="kx-live-value"> Selected: <code>{month ?? 'null'}</code> </div> </MonthPicker> ); }
Parts
MonthPicker reuses DatePicker's building blocks for everything except the grid:
| Part | Source | Purpose |
|---|---|---|
MonthPicker.Root | wraps DatePicker.Root | controlled/uncontrolled state, displayTimezone, disabled rules, dir (RTL mirrors the month grid) |
MonthPicker.Input | = DatePicker.Input | text input (combobox role) |
MonthPicker.Trigger | = DatePicker.Trigger | icon button |
MonthPicker.Popover | = DatePicker.Popover | Floating UI positioning |
MonthPicker.Grid | new | 12-month grid with prev/next year navigation |
Timezone
When displayTimezone is set, the committed value is the civil midnight of the selected month's first day in that zone (UTC-ISO form). The grid highlighting honors the timezone so the right month stays marked as selected even when stored as a zone-adjusted UTC string.
<MonthPicker value={month} onChange={setMonth} displayTimezone="Asia/Seoul">
<MonthPicker.Input />
<MonthPicker.Popover>
<MonthPicker.Grid />
</MonthPicker.Popover>
</MonthPicker>
Locale
Month names follow the locale prop (BCP 47). The built-in getMonthName helper uses Intl.DateTimeFormat so any locale supported by the JS runtime works without extra dependencies.
<MonthPicker locale="ko-KR">
<MonthPicker.Input />
<MonthPicker.Popover>
<MonthPicker.Grid />
</MonthPicker.Popover>
</MonthPicker>
Disabled rules
Restrict selectable months using the same DisabledRule syntax as DatePicker. A month is disabled only when the rules exclude every day in that month; a rule that blocks just the first day does not disable the remaining month.
function DisabledMonthPicker() { const [month, setMonth] = React.useState(null); const headerCls = { header: 'kx-live-header', title: 'kx-live-title', navButton: 'kx-live-nav', }; return ( <MonthPicker value={month} onChange={setMonth} disabled={[ { before: '2026-01-01T00:00:00.000Z' }, { after: '2026-12-31T00:00:00.000Z' }, ]} > <div className="kx-live-row"> <MonthPicker.Input className="kx-live-input" placeholder="2026 only" /> <MonthPicker.Trigger className="kx-live-trigger" aria-label="Open month picker" /> </div> <MonthPicker.Popover className="kx-live-popover"> <MonthPicker.Grid classNames={{ ...headerCls, grid: 'kx-live-month-grid', month: 'kx-live-my-cell', monthSelected: 'kx-live-my-selected', monthCurrent: 'kx-live-my-current', monthDisabled: 'kx-live-disabled', }} /> </MonthPicker.Popover> <div className="kx-live-value"> Selected: <code>{month ?? 'null'}</code> </div> </MonthPicker> ); }
<MonthPicker
value={month}
onChange={setMonth}
disabled={[
{ before: '2026-01-01T00:00:00.000Z' },
{ after: '2026-12-31T00:00:00.000Z' },
]}
>
<MonthPicker.Input placeholder="2026 only" />
<MonthPicker.Popover>
<MonthPicker.Grid />
</MonthPicker.Popover>
</MonthPicker>
Uncontrolled
For simple forms where you don't need React state:
<MonthPicker defaultValue="2026-04-01T00:00:00.000Z">
<MonthPicker.Input name="billingMonth" />
<MonthPicker.Popover>
<MonthPicker.Grid />
</MonthPicker.Popover>
</MonthPicker>
MonthPicker.Input inherits DatePicker.Input's native form contract. Passing
name renders a hidden input containing the month-start UTC ISO value; the
visible formatted input is not submitted under that name.
Event callbacks
| Prop | Signature | Fires when |
|---|---|---|
onChange | (value: ISODateString | null) => void | A month is committed (click or input typed). |
onOpenChange | (isOpen: boolean) => void | The popover opens or closes. |
onCalendarNavigate | (viewMonth: ISODateString) => void | The grid navigates to a different year. |
Props
MonthPicker Root accepts the same props as DatePicker.Root. The only difference is the default displayFormat — otherwise disabled, readOnly, weekStartsOn, locale, displayTimezone, labels, adapter, onOpenChange, and onCalendarNavigate all behave identically. See DatePicker for the full reference.
Grid classNames
<MonthPicker.Grid
classNames={{
root: '',
header: '',
title: '',
navButton: '',
grid: '',
gridRow: '',
month: '',
monthSelected: '',
monthCurrent: '',
monthDisabled: '',
}}
/>
Each month cell emits data-selected, data-current, and data-focused (active-only). See Styling.