Skip to content
kerfjsGitHub

MonthPicker

Pick a month from a year grid.

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import { MonthPicker, Stack, Text } from '@kerf-js/veneer'import type { IsoDate, IsoMonth } from '@kerf-js/veneer'export function Invoice() {  const month = state<IsoMonth | null>('2026-10')  return (    <Stack gap="related">      <MonthPicker        label="Invoice month"        today={TODAY}        value={month}        max="2026-11"      />      <Text        subtle        role="caption"      >        Showing {month.value ?? 'every month'}      </Text>    </Stack>  )}const TODAY: IsoDate = '2026-11-02'

Installation

npm install @kerf-js/veneer

Usage

tsx
import { MonthPicker } from '@kerf-js/veneer'

Sizes

size matches the other fields in a form.

tsx
import { MonthPicker, Stack } from '@kerf-js/veneer'import type { IsoDate } from '@kerf-js/veneer'export function Sizes() {  return (  <Stack gap="group">    <MonthPicker      label="Small"      size="sm"      today={TODAY}      defaultValue="2026-10"    />    <MonthPicker      label="Medium"      today={TODAY}      defaultValue="2026-10"    />    <MonthPicker      label="Large"      size="lg"      today={TODAY}      defaultValue="2026-10"    />  </Stack>  )}const TODAY: IsoDate = '2026-11-02'

Limits

min and max turn off the months outside them, and the year arrows stop at their years.

tsx
import { MonthPicker } from '@kerf-js/veneer'import type { IsoDate } from '@kerf-js/veneer'export function Range() {  return (  <MonthPicker    label="Statement"    hint="From June 2025, when the workspace began."    today={TODAY}    defaultValue="2025-08"    min="2025-06"    max="2026-10"  />  )}const min = 60_000const TODAY: IsoDate = '2026-11-02'

Another language

locale names the months. The value stays 2026-03 whatever the language.

tsx
import { MonthPicker } from '@kerf-js/veneer'import type { IsoDate } from '@kerf-js/veneer'export function Locale() {  return (  <MonthPicker    label="Abrechnungsmonat"    locale="de-DE"    today={TODAY}    defaultValue="2026-03"  />  )}const TODAY: IsoDate = '2026-11-02'

Hint, error, read-only, disabled

The same hint, error, readonly and disabled as every field.

tsx
import { Grid, MonthPicker } from '@kerf-js/veneer'import type { IsoDate } from '@kerf-js/veneer'import { Stack, Text } from '@kerf-js/veneer'export function States() {  return (  <Grid    cols={2}    gap="group"  >    <Cell name="Empty">      <MonthPicker        label="Month"        today={TODAY}      />    </Cell>    <Cell name="Hint">      <MonthPicker        label="Month"        today={TODAY}        hint="Invoices go out on the 1st."      />    </Cell>    <Cell name="Invalid">      <MonthPicker        label="Month"        today={TODAY}        error="Choose a month to export."      />    </Cell>    <Cell name="Read-only">      <MonthPicker        label="Month"        today={TODAY}        defaultValue="2026-10"        readonly      />    </Cell>    <Cell name="Disabled">      <MonthPicker        label="Month"        today={TODAY}        defaultValue="2026-10"        disabled      />    </Cell>  </Grid>  )}const Cell = ({  name,  children,}: {  name: string  children?: unknown}) => (  <Stack gap="tight">    <Text      role="caption"      subtle    >      {name}    </Text>    {children}  </Stack>)const TODAY: IsoDate = '2026-11-02'

Filter invoices

The month filters the list and its total. Show all sets the value back to null.

tsx
import { state } from '@kerf-js/core'import {  Badge,  Button,  MonthPicker,  Row,  Stack,  Text} from '@kerf-js/veneer'import type { IsoDate, IsoMonth } from '@kerf-js/veneer'export function Invoices() {  const month = state<IsoMonth | null>('2026-09')  const shown = () =>    INVOICES.filter(      (i) =>        !month.value || i.date.startsWith(month.value),    )  const total = () =>    usd(shown().reduce((t, i) => t + i.amount, 0))  return (    <Stack gap="group">      <Row        gap="related"        wrap={false}      >        <MonthPicker          label="Invoices for"          today={TODAY}          value={month}          min="2026-07"          max="2026-10"        />        <Button          emphasis="ghost"          tone="neutral"          aria-disabled={month.value ? undefined : 'true'}          onClick={() => (month.value = null)}        >          Show all        </Button>      </Row>      <Stack        gap="related"        divided      >        {shown().map((i) => (          <Row            key={i.id}            justify="between"            gap="group"          >            <Stack gap="hair">              <Text role="label">{i.id}</Text>              <Text                role="caption"                subtle              >                Issued {i.date}              </Text>            </Stack>            <Row gap="related">              <Badge tone="positive">{i.status}</Badge>              <Text>{usd(i.amount)}</Text>            </Row>          </Row>        ))}      </Stack>      <Text        subtle        role="caption"      >        {shown().length} of {INVOICES.length}        {' invoices, '}        {total()} in all      </Text>    </Stack>  )}const INVOICES = [  {    id: 'INV-2026-10',    date: '2026-10-01',    amount: 8560,    status: 'Paid',  },  {    id: 'INV-2026-09',    date: '2026-09-01',    amount: 8230,    status: 'Paid',  },  {    id: 'INV-2026-08',    date: '2026-08-01',    amount: 8010,    status: 'Paid',  },  {    id: 'INV-2026-07',    date: '2026-07-01',    amount: 7620,    status: 'Paid',  },]const min = 60_000const TODAY: IsoDate = '2026-11-02'const usd = (n: number) =>  `$${n.toLocaleString('en-US')}`

When to use

Use it for

  • Statements, invoices and reports that run by the calendar month

Reach for something else

Accessibility

Arrows
Move between months
PageUp/PageDown
Change the year
EnterorSpace
Picks the month

API Reference

PropTypeDefaultDescription
labelstring—The accessible name, drawn as the floating border label (or above, classic). Required.
hintunknown—One line of help under the control.
errorunknown—Shown instead of the hint; marks the field invalid.
iconIconRef—Leading icon: it sweeps with the border.
prefixunknown—Text or an element inside the field before the value, such as $.
suffixunknown—Text or an element inside the field after the value, such as kg.
variant"floating" | "classic"—floating (default, or the app's veneer({ fields })) · classic: the label above.
appearance"line"—line: only a bottom line under the value, no box or fill. The boxed field otherwise.
size"sm" | "md" | "lg"'md'Control size. md by default.
clearablebooleanfalseA clear button (shown once there is a value).
counternumber—Maximum length, shown as a counter.
idstring—The element id. One is made for you if you leave it out.
disabledbooleanfalseTurns it off, and takes it out of the tab order.
readonlybooleanfalseShows the value and lets it be selected and copied, but not changed.
requiredbooleanfalseThe form will not submit without it.
namestring—The name it posts under in a form.
localestring—The language and region used to format values.
todaystring—Pins "today" (server render, tests); the browser's clock otherwise.
presentationPresentation—auto opens a popover, or a sheet when the viewport is under 600px wide; popover and sheet force one.
value`${number}-${number}` | State<`${number}-${number}` | null> | null—The current value, when you own the state.
defaultValue`${number}-${number}` | null—The starting value, when the component owns the state.
openboolean | State<boolean>—A state box that holds whether it is open.
defaultOpenbooleanfalseStarts open, when the component keeps its own open state.
min`${number}-${number}`—The lowest allowed value.
max`${number}-${number}`—The highest allowed value.
EventTypeDefaultDescription
onInput((e: InputEvent & { currentTarget: HTMLInputElement & HTMLTextAreaElemen…—Called on each keystroke.
onValueChange((v: `${number}-${number}` | null, reason: ChangeReason) => void)—Called with each new value, and whether a pointer, a key or code made the change.
onOpenChange((o: boolean) => void)—Called with true when it opens and false when it closes.

RelatedWeek pickerPeriod picker