Month Picker React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React month picker for one month or a range from a year grid, with quarters and presets, for reports, billing periods and expiry dates.
Click the field to open a panel under it with the year at the top and its twelve months in four rows of three; the current month carries a small dot. The arrows beside the year move one year (the grid slides that way), and the year itself opens a grid of twelve years for long jumps (its arrows move twelve years). In single mode a click on a month sets it, the fill glides to it and the panel closes. In range mode the first click sets the first month, moving the pointer previews the band, and the second click sets the last month (a month before the first swaps them); months past maxMonths or beyond a disabled month cannot be picked while the band is open. The range commits on the second click and the panel stays open until Escape, Enter or a click outside, with a summary like "Jul - Sep 2026 · 3 months" under the grid. Quarter chips (Q1 to Q4, T1 to T4 in French or Spanish) pick three months at once, or the quarter's first month in single mode, and presets (This month, Last 3 months, This year...) sit in a column on the left, clamped to min and max. The clear button empties the field. Keyboard: Enter, Space or ArrowDown on the field open the panel with focus on the selected (else the current) month; arrows move one month, Up / Down three, Home / End the row's ends, PageUp / PageDown a year, Enter or Space pick, and Enter on the last month of a range closes the panel. Escape first leaves the years grid, then drops a half-picked range, then closes and returns focus to the field. On a phone the field opens a bottom sheet with the title, the presets as a row of chips, 48px month cells and the quarters; single mode commits on tap and closes, range mode keeps a draft in the sheet until Apply, and Cancel or dragging the sheet away keeps the old value. name submits YYYY-MM, or name.from and name.to as ISO dates.
Month Picker is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add month-picker, together with Button, Drawer, Field, Date utils, Relative time and 6 more and the npm packages @radix-ui/react-popover, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add month-pickerMonth Picker is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import MonthPicker from @/components/ui/month-picker.
import { useState } from "react";
import { MonthPicker, type DateRange } from "@/components/ui/month-picker";
export function SalesReport() {
const [period, setPeriod] = useState<DateRange>({ from: new Date(2026, 6, 1), to: new Date(2026, 8, 30) });
const [month, setMonth] = useState<Date | null>(new Date(2026, 8, 1));
return (
<div className="flex flex-col gap-4">
<MonthPicker
mode="range"
label="Period"
presets
max={new Date()}
value={period}
onValueChange={setPeriod}
name="period"
labels={{ apply: "Done" }}
/>
<MonthPicker label="Billing month" value={month} onValueChange={setMonth} min={new Date(2024, 0, 1)} max={new Date()} />
</div>
);
}Props
modedefault"single""single" | "range"single picks one month (value: its first day at 00:00); range picks a span of months (value: from the first day of the first month to the last day of the last one, ready for a query).
valueDate | null (single) | DateRange (range)The month or range, controlled. A date in the middle of a month shows as its month and is not rewritten; a value outside min / max or on a disabled month shows as given and flags the field invalid.
defaultValuedefault"null / { from: null, to: null }"Date | null (single) | DateRange (range)The starting value while uncontrolled. A form reset returns to it.
onValueChange(value: MonthPickerValue<mode>) => voidCalled with the new month (single) or the complete range (range: on the second click, a quarter or a preset; on phones on Apply). A half-picked range never reaches it. null or { from: null, to: null } when cleared.
labeldefault""ReactNodeThe field label. Plain text also titles the phone sheet and names the panel.
descriptiondefault""ReactNodeHelper text under the field, linked with aria-describedby.
errordefault""ReactNodeReplaces the description with the error and marks the field invalid.
placeholderdefault""stringThe empty trigger text. Empty uses "Pick a month" (single) or "Pick months" (range).
presetsdefaultfalseboolean | MonthPreset[]true: This month, Last month, Last 3 / 6 / 12 months, This year and Last year in range mode (This month and Last month in single), clamped to min / max and off when they break maxMonths or touch a disabled month. Or your own list of { label, value: Date | DateRange | () => Date | DateRange, key? }. A column on the left from 640px, a chip rail on phones.
localedefault""stringMonth names, the trigger text and the quarter letter (Q in English, T in Romanian, French, Spanish, Italian, Portuguese). Empty uses the nearest LocaleProvider, else the page's language.
sheetTitledefault""stringThe phone sheet title when the label is not plain text. Empty uses labels.dialog ("Choose month").
displayFormatdefault"{ month: "long", year: "numeric" } (single), { month: "short", year: "numeric" } (range)"Intl.DateTimeFormatOptionsThe trigger text through Intl: "septembrie 2026"; ranges through formatRange, the shared year folded ("iul. - sept. 2026", across years "nov. 2025 - feb. 2026").
formatValue(value: MonthPickerValue<mode>, locale: string) => stringYour own trigger text, for formats Intl does not write, like a card expiry "09/28".
icondefault"<CalendarDays />"ReactNodeThe leading icon of the trigger.
labelsdefault"DEFAULT_MONTH_PICKER_LABELS"Partial<MonthPickerLabels>Every string: previousYear, nextYear, previousYears, nextYears, chooseYear, quarter(q), quarters, presets, the preset names (thisMonth, lastMonth, last3Months, last6Months, last12Months, thisYear, lastYear), months(count) for the summary, pickStart, pickEnd, apply, cancel, clear, dialog.
sizedefault"md""sm" | "md" | "lg"The field height: 32 / 40 / 48px (44px on touch for md), matching inputs and selects.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The field box: a soft fill, a hairline outline, a bottom line, or nothing until hover.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"The field corners, per size like the button.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The selected months, the range band, the quarter chips and Apply.
colordefault""stringAny CSS color for the selection; replaces the tone.
monthFormatdefault"short""short" | "long"The month cells: "Sept." or "Septembrie".
classNamesPartial<Record<MonthPickerSlot, string>>Classes per part: field, label, description, error, box, trigger, icon, value, clear, chevron, content, panel, header, title, nav, grid, month, years, year, quarters, quarter, presets, preset, footer, summary.
minDateThe first month that can be picked; earlier months are disabled and the year arrows stop there.
maxDateThe last month that can be picked. A max before min is ignored.
quartersdefaultfalsebooleanThe Q1 to Q4 chips under the grid. Left unset they show in range mode (three months at once) and hide in single mode, where true adds them and a quarter picks its first month.
maxMonthsdefault0numberThe longest range in months; while you pick the end, months farther away are disabled. 0 = no limit.
clearabledefaultfalsebooleanA clear button inside the field while it has a value.
closeOnSelectdefaulttruebooleanSingle mode closes after a pick. Range mode always stays open until Escape, Enter or a click outside.
nowDate | numberWhat this month means: the current-month dot and the presets. Left out, the live clock (useNow); pin it for screenshots.
namedefault""stringHidden inputs for forms: single submits YYYY-MM, range submits name.from and name.to as ISO dates.
disableddefaultfalseboolean or (month: Date) => booleantrue dims the whole field and never opens it. A test disables single months instead (closed accounting periods): it gets each month's first day.
readOnlydefaultfalsebooleanThe value stays visible but the panel never opens.
requireddefaultfalsebooleanThe mark after the label and aria-required on the trigger.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
openbooleanThe panel, controlled. Pair it with onOpenChange.
defaultOpendefaultfalsebooleanOpens the panel on mount while uncontrolled.
onOpenChange(open: boolean) => voidCalled when the panel or sheet opens or closes.
inlinedefaultfalsebooleanThe panel sits on the page without a trigger (a raised surface with the grid, quarters, presets and the range summary).
responsivedefaulttruebooleanA bottom sheet below 768px instead of the popover.
sidedefault"bottom""top" | "right" | "bottom" | "left"Where the popover opens; it flips when there is no room.
aligndefault"start""start" | "center" | "end"How the popover lines up with the field.
containerHTMLElement | nullThe portal target of the popover and the sheet (default document.body).
idstringThe trigger's id; the label points at it.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, hideLabel.
refRef<HTMLButtonElement> (a useRef object or an (el) => void callback)The trigger button (a regular prop in React 19).
Parts
MonthPickerPanel
componentThe grid alone on a raised panel, for inline use and composition: mode, value / defaultValue / onValueChange, min, max, disabled, quarters, presets, maxMonths, monthFormat, tone, color, now, locale, labels, classNames, autoFocus. Props: MonthPickerPanelProps<M>.
DEFAULT_MONTH_PICKER_LABELS
constantThe English strings; spread them to translate a few. Type: MonthPickerLabels.
startOfMonthDate
functionThe first / last day of the date's month at 00:00. Signature: (date: Date) => Date.
endOfMonthDate
functionThe first / last day of the date's month at 00:00. Signature: (date: Date) => Date.
getQuarter
functionThe quarter a date falls in. Signature: (date: Date) => 1 | 2 | 3 | 4.
quarterRange
functionThe quarter as a range: its first day to its last day (Q3 2026: Jul 1, 2026 to Sep 30, 2026). Signature: (year: number, quarter: number) => { from: Date; to: Date }.
Examples
Billing month
Single month between Jan 2024 and this month, with presets and clear.
Card expiry
A custom formatValue writes "09/28"; months before now are closed.
Fiscal quarter
Range mode with quarter chips and maxMonths 3, shown as Q3 2026.
Inline budget planner
The panel on the page with custom presets, brand tone and a 6 month cap.
Closed accounting periods
A disabled test closes every month before July 2026.
en-US vs fr-FR
Long month names and the quarter letter (Q or T) follow the locale; the range spans two years.
Tones and sizes
sm outline brand, md success range, lg custom color.
Dependencies
Files
- components/ui/month-picker.tsx
FAQ
Is the React Month Picker component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Month Picker?
Run npx wingo-ui@latest add month-picker. It also adds the Wingo UI items it builds on (Button, Drawer, Field, Date utils, Relative time and 6 more) and the npm packages @radix-ui/react-popover, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/month-picker.
How does Month Picker behave on phones?
A bottom sheet below 768px instead of the popover. The responsive prop controls this and is on by default.
See also
More React input components
Articles using this component
- React Month Picker: Months, Quarters and Ranges
Component guides8 min read