Skip to main content

YearPicker

Year selector. The value is January 1 of the selected year in UTC-ISO form. For example, picking 2026 yields "2026-01-01T00:00:00.000Z".

YearPicker demo: choosing a year from the decade grid
Styling shown is demo-only. Kalyx ships zero CSS.
import { YearPicker } from '@kalyx/react';

Anatomy​

<YearPicker> {/* Root: value = Jan 1 of the year, UTC */}
<YearPicker.Input /> {/* combobox <input>, parses "YYYY" */}
<YearPicker.Trigger /> {/* button that toggles the popover */}
<YearPicker.Popover> {/* Floating-UI portal, role="dialog" */}
<YearPicker.Grid /> {/* paginated grid of years, role="grid" */}
</YearPicker.Popover>
</YearPicker>

Input and Trigger are re-exported from DatePicker and read the YearPicker context.

Basic usage​

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

function Example() {
const [year, setYear] = useState<ISODateString | null>(null);
return (
<YearPicker value={year} onChange={setYear}>
<YearPicker.Input placeholder="YYYY" />
<YearPicker.Popover>
<YearPicker.Grid />
</YearPicker.Popover>
</YearPicker>
);
}

The default displayFormat is "yyyy".

Try it live​

The live editor runs with React and all Kalyx components in scope, so import lines are omitted. Copy them in when porting to your project; see the full imports in the non-live blocks above.

Live Editor
function BasicYearPicker() {
  const [year, setYear] = React.useState(null);
  const headerCls = {
    header: 'kx-live-header',
    title: 'kx-live-title',
    navButton: 'kx-live-nav',
  };
  return (
    <YearPicker value={year} onChange={setYear}>
      <div className="kx-live-row">
        <YearPicker.Input className="kx-live-input" placeholder="YYYY" />
        <YearPicker.Trigger className="kx-live-trigger" aria-label="Open year picker" />
      </div>
      <YearPicker.Popover className="kx-live-popover">
        <YearPicker.Grid
          classNames={{
            ...headerCls,
            grid: 'kx-live-year-grid',
            year: 'kx-live-my-cell',
            yearSelected: 'kx-live-my-selected',
            yearCurrent: 'kx-live-my-current',
          }}
        />
      </YearPicker.Popover>
      <div className="kx-live-value">
        Selected: <code>{year ?? 'null'}</code>
      </div>
    </YearPicker>
  );
}
Result
Loading...

Parts​

PartSourcePurpose
YearPicker.Rootwraps DatePicker.Rootcontrolled/uncontrolled state, displayTimezone, disabled rules, dir (RTL mirrors the year grid)
YearPicker.Input= DatePicker.Inputtext input (combobox role)
YearPicker.Trigger= DatePicker.Triggericon button
YearPicker.Popover= DatePicker.PopoverFloating UI positioning
YearPicker.Gridnew12-year decade grid with prev/next decade navigation

The grid displays the decade block containing the current year (e.g., 2016-2027 when the value is 2026). Navigate by 12 years at a time using the header buttons.

Timezone​

When displayTimezone is set, year highlighting is timezone-aware. This matters when the stored UTC-ISO has been shifted to represent civil midnight in a non-UTC zone.

<YearPicker value={year} onChange={setYear} displayTimezone="America/New_York">
<YearPicker.Input />
<YearPicker.Popover>
<YearPicker.Grid />
</YearPicker.Popover>
</YearPicker>

Disabled rules​

Restrict selectable years. A year is disabled only when the rules exclude every day in that year; a rule that blocks January 1 alone does not disable the remaining year.

Live Editor
function DisabledYearPicker() {
  const [year, setYear] = React.useState(null);
  const headerCls = {
    header: 'kx-live-header',
    title: 'kx-live-title',
    navButton: 'kx-live-nav',
  };
  return (
    <YearPicker
      value={year}
      onChange={setYear}
      disabled={[
        { before: '2020-01-01T00:00:00.000Z' },
        { after: '2030-01-01T00:00:00.000Z' },
      ]}
    >
      <div className="kx-live-row">
        <YearPicker.Input className="kx-live-input" placeholder="2020–2030" />
        <YearPicker.Trigger className="kx-live-trigger" aria-label="Open year picker" />
      </div>
      <YearPicker.Popover className="kx-live-popover">
        <YearPicker.Grid
          classNames={{
            ...headerCls,
            grid: 'kx-live-year-grid',
            year: 'kx-live-my-cell',
            yearSelected: 'kx-live-my-selected',
            yearCurrent: 'kx-live-my-current',
            yearDisabled: 'kx-live-disabled',
          }}
        />
      </YearPicker.Popover>
      <div className="kx-live-value">
        Selected: <code>{year ?? 'null'}</code>
      </div>
    </YearPicker>
  );
}
Result
Loading...
<YearPicker
value={year}
onChange={setYear}
disabled={[
{ before: '2020-01-01T00:00:00.000Z' },
{ after: '2030-01-01T00:00:00.000Z' },
]}
>
<YearPicker.Input placeholder="2020–2030" />
<YearPicker.Popover>
<YearPicker.Grid />
</YearPicker.Popover>
</YearPicker>

Uncontrolled​

<YearPicker defaultValue="2026-01-01T00:00:00.000Z">
<YearPicker.Input name="fiscalYear" />
<YearPicker.Popover>
<YearPicker.Grid />
</YearPicker.Popover>
</YearPicker>

YearPicker.Input inherits DatePicker.Input's native form contract. Passing name renders a hidden input containing the year-start UTC ISO value; the visible formatted input is not submitted under that name.

Event callbacks​

PropSignatureFires when
onChange(value: ISODateString | null) => voidA year is committed (click or input typed).
onOpenChange(isOpen: boolean) => voidThe popover opens or closes.
onCalendarNavigate(viewMonth: ISODateString) => voidThe grid navigates to a different decade.

Props​

YearPicker Root accepts the same props as DatePicker.Root. Only the default displayFormat differs. See DatePicker for the full reference.

Grid classNames​

<YearPicker.Grid
classNames={{
root: '',
header: '',
title: '',
navButton: '',
grid: '',
gridRow: '',
year: '',
yearSelected: '',
yearCurrent: '',
yearDisabled: '',
}}
/>

Each year cell emits data-selected, data-current, and data-focused (active-only). See Styling.