MonthPicker
Pick a month from a year grid.
SourcePackage @weft-js/warp
Preview
tsx
import { state } from '@weft-js/core'import { MonthPicker, Stack, Text } from '@weft-js/warp'import type { IsoDate, IsoMonth } from '@weft-js/warp'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 @weft-js/warpUsage
tsx
import { MonthPicker } from '@weft-js/warp'Sizes
size matches the other fields in a form.
tsx
import { MonthPicker, Stack } from '@weft-js/warp'import type { IsoDate } from '@weft-js/warp'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 '@weft-js/warp'import type { IsoDate } from '@weft-js/warp'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 '@weft-js/warp'import type { IsoDate } from '@weft-js/warp'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 { MonthPicker, Stack, Text, Grid } from '@weft-js/warp'import type { IsoDate } from '@weft-js/warp'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 TODAY: IsoDate = '2026-11-02'const Cell = (p: { name: string; children?: unknown }) => ( <Stack gap="tight"> <Text role="caption" subtle>{p.name}</Text> {p.children} </Stack>)Filter invoices
The month filters the list and its total. Show all sets the value back to null.
tsx
import { state } from '@weft-js/core'import { Badge, Button, MonthPicker, Row, Stack, Text } from '@weft-js/warp'import type { IsoDate, IsoMonth } from '@weft-js/warp'export function Invoices() { const month = state<IsoMonth | null>('2026-09') const shown = () => INVOICES.filter(i => !month.value || i.date.startsWith(month.value)) 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, {usd(shown().reduce((t, i) => t + i.amount, 0))} 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
- PeriodPicker, when people may want a quarter or a year too
- DateRangeField, when the range can start on any day
Accessibility
- Arrows
- Move between months
- PageUp/PageDown
- Change the year
- EnterorSpace
- Picks the month
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The accessible name, drawn as the floating border label (or above, classic). Required. |
hint | unknown | — | One line of help under the control. |
error | unknown | — | Shown instead of the hint; marks the field invalid. |
icon | IconRef | — | Leading icon: it sweeps with the border. |
prefix | unknown | — | Text or an element inside the field before the value, such as $. |
suffix | unknown | — | Text or an element inside the field after the value, such as kg. |
variant | "floating" | "classic" | — | floating (default, or the app's warp({ 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. |
clearable | boolean | false | A clear button (shown once there is a value). |
counter | number | — | Maximum length, shown as a counter. |
id | string | — | The element id. One is made for you if you leave it out. |
disabled | boolean | false | Turns it off, and takes it out of the tab order. |
readonly | boolean | false | Shows the value and lets it be selected and copied, but not changed. |
required | boolean | false | The form will not submit without it. |
name | string | — | The name it posts under in a form. |
locale | string | — | The language and region used to format values. |
today | string | — | Pins "today" (server render, tests); the browser's clock otherwise. |
presentation | Presentation | — | 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. |
open | boolean | State<boolean> | — | A state box that holds whether it is open. |
defaultOpen | boolean | false | Starts open, when the component keeps its own open state. |
min | `${number}-${number}` | — | The lowest allowed value. |
max | `${number}-${number}` | — | The highest allowed value. |
| Event | Type | Default | Description |
|---|---|---|---|
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