# Monthpicker Field
Lets users input a month and year using a calendar popup or free text.
Section: components
Docs: https://nebula.kaluza.com/components/monthpicker-field
---
## When to use
- When asking the user to select a month where the specific day is not meaningful: for example, a billing period or usage period
- When it is useful for the user to browse nearby months for context, or to see which months are available
- When months could span far into the past or future, and scrolling through individual days would be inefficient
## When not to use
- **When the user needs an exact date** - use [Datepicker Field](/components/datepicker-field) instead
- **When choosing from a small, fixed set of months** - use [Select Field](/components/select-field) or [Radio](/components/radio) instead. Browsing a calendar adds interaction cost when there is nothing to browse
- **To indicate that a field is unavailable** - the disabled state is not supported. Use [Summary List](/components/summary-list) to show values that cannot be edited, or conditionally hide the field if it is not relevant in the current context.
## Anatomy
1. **Field**: based on Text Field, contains label, hint text, input, and error state
2. **Calendar button**: opens the calendar popover
3. **Year controls**: moves the year forwards and backwards
4. **Selected month**
5. **Today's month**
6. **This month button**: selects today's month and closes the calendar popover
7. **Clear button**: clears the current entry and closes the calendar popover
8. **Calendar popover**: always a fixed width, always aligned to the left edge of the field
## Basic usage
```tsx
```
## Validation
MonthpickerField uses the `mm/yyyy` format. The input is a free-text field, so always validate on submit. Use `getMonthpickerDate` to parse and validate the string: it returns a `Date` pinned to the 1st of the selected month, or `null` if invalid.
```tsx
const Example = () => {
const [errors, setErrors] = useState([]);
const onSubmit = (e) => {
e.preventDefault();
const newErrors = [];
const { monthpicker } = Object.fromEntries(new FormData(e.currentTarget));
const monthpickerDate = getMonthpickerDate(monthpicker)
if (!monthpickerDate) {
newErrors.push({ id: 'monthpicker', message: 'Enter a valid month in the format mm/yyyy'});
}
setErrors(newErrors);
if (!newErrors.length) {
alert('success');
}
}
return (
);
}
```
## Unavailable months
Use `isMonthUnavailable` to mark months as unavailable in the popover. This does not prevent manual entry, so always validate on submit too.
```tsx
date.getTime() < Date.now()}
/>
```
## Month ranges
Use two Monthpicker Fields side by side. Set months before the start month as unavailable in the end field, and reset the end month when the start changes.
Align the two fields to the bottom of their container so inputs stay aligned when one has hint text or an error.
```tsx
const Example = () => {
const [startMonth, setStartMonth] = useState('');
const [endMonth, setEndMonth] = useState('');
const isMonthUnavailable = (date) => {
if (!startMonth) {
return false;
}
const [m, y] = startMonth.split('/');
const start = new Date(y + '/' + m + '/01');
return date.getTime() < start.getTime();
};
return (
{
setStartMonth(value);
setEndMonth('');
}}
/>
);
}
```
## Common form features
Monthpicker Field shares validation, hint text, required/optional marking, error display, and field width behaviour with other form fields. See the [forms pattern guide](/patterns/forms) for full guidance.
## Content
### Labels
Use a noun that describes the month, not a sentence fragment ("Billing month", not "Billing for"). Keep labels to two or three words.
### Hints
Use hints to describe a constraint on the month. The field already shows the format as placeholder text, so do not repeat it as a hint.
## OverlayProvider
Monthpicker Field uses React Aria. Wrap your app in `OverlayProvider` so the page behind the calendar popover is hidden from screen readers while it is open.
## Rules and guidance
### Usage
- **MonthpickerField** MUST NOT be used when the user needs to provide an exact date. Rationale: Use DatepickerField instead. MonthpickerField captures month and year only; the day is always set to the 1st. [id: monthpickerfield.use-for-month-not-date; testability: context]
- **MonthpickerField** MUST NOT be rendered in a disabled state. Rationale: Disabled fields are invisible to screen readers, cannot be focused, and provide no feedback. Use read-only or hide the field until it is relevant. [id: monthpickerfield.no-disabled; testability: auto]
- **MonthpickerField** SHOULD NOT be used when the user is choosing from a small, fixed set of months. Rationale: A calendar popover adds interaction cost when there is nothing to browse. Use Select Field or Radio for short, fixed option sets. [id: monthpickerfield.no-fixed-set; testability: context]
### Validation
- **MonthpickerField** MUST validate the field value on form submission using getMonthpickerDate. Rationale: The input is a free-text field. Users can type any string, so client-side validation is required before using the value. [id: monthpickerfield.validate-on-submit; testability: auto]
- If isMonthUnavailable is provided: **MonthpickerField** MUST still validate the entered value on submit when isMonthUnavailable is used. Rationale: isMonthUnavailable disables months in the popover only. It does not prevent a user from typing a restricted month directly into the text input. [id: monthpickerfield.unavailable-months-validate; testability: auto]
### Composition
- **MonthpickerField** MUST be rendered inside an OverlayProvider. Rationale: OverlayProvider hides the page behind the calendar popover from screen readers. Without it, AT users can read content that is not interactable. [id: monthpickerfield.overlay-provider; testability: manual]
### Content
- **MonthpickerField** SHOULD use a noun phrase as the label, not a sentence fragment. Rationale: Sentence fragments ("Billing for") make error messages nonsensical ("Enter a valid billing for"). Noun phrases ("Billing month") work in all contexts. [id: monthpickerfield.label-noun; testability: manual]
- **MonthpickerField** SHOULD NOT repeat the mm/yyyy format in hint text. Rationale: The field already shows the format as placeholder text. Repeating it in the hint adds noise. [id: monthpickerfield.no-format-hint; testability: manual]
### Placement
- If two MonthpickerFields are used side by side for a month range: **MonthpickerField** SHOULD align side-by-side range fields to the bottom of their container. Rationale: Fields with hint text or errors are taller than those without. Bottom alignment keeps the inputs visually aligned when field heights differ. [id: monthpickerfield.range-alignment; testability: manual]
### Interaction
- If two MonthpickerFields are used side by side for a month range: **MonthpickerField** SHOULD reset the end month when the start month changes in a range pair. Rationale: Without a reset, the end month can be earlier than the start month, putting the user in an unrecoverable state. [id: monthpickerfield.range-reset; testability: manual]
## Accessibility
### In brief
Monthpicker Field is a text input for typing a month as mm/yyyy, with a calendar button that opens a grid of twelve months in a dialog. A screen reader announces the label, then a text field, and the button is a separate stop that opens the picker.
### What Nebula provides
- A real `label` linked to the text input, `aria-required` unless the field is `optional`, and an `error` linked with `aria-invalid` and `aria-describedby`.
- A calendar button with `aria-haspopup="dialog"` and `aria-expanded`, built on React Aria.
- A dialog that keeps focus inside while it is open, returns focus when it closes, closes with `Esc` or a click outside, and has a hidden dismiss button for screen reader users.
- A grid of months, each named with its month and year (for example "January 2024"), with the selected month marked. Months marked unavailable are dimmed and cannot be selected in the picker.
### Keyboard behaviour
The input and then the calendar button are tab stops. In the picker, focus lands on the selected month, or the current one, and only that month is a tab stop; the arrow keys move between months, `Home` and `End` go to January and December, `Enter` or `Space` selects a month and `Esc` closes it.
#### Focus order
1. The month input
2. The calendar button
3. Inside the open picker: the year buttons, the month grid, then This month and Clear
#### Interactions
- **Tab** - Moves focus to the input, then to the calendar button
- **Enter or Space** - Opens the picker from the calendar button
### Accessible design
#### Always put the month format in the hint, not only in the placeholder
The placeholder (mm/yyyy) disappears as soon as the user types and is shown at half opacity, so a user who is partway through a month loses the format. Write it in the hint, for example "For example, 02/2024".
WCAG: [3.3.2 Labels or Instructions](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html)
#### If some months are unavailable, say which in the hint
The picker only dims them, and a user who types a month is not stopped. A short hint such as "Within the last 12 months" tells every user the rule before they choose.
WCAG: [3.3.2 Labels or Instructions](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html)
### Accessible development
#### Always validate the typed text on submit, and say what is wrong
The input accepts any text and `isMonthUnavailable` does not apply to it. Check it with `getMonthpickerDate` and show an `error` that names the format or the rule it broke.
WCAG: [3.3.1 Error Identification](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html) [3.3.3 Error Suggestion](https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html)
#### Always pass errors through `error`, or through a `Form` with the field `name`
This is what sets `aria-invalid` and links the message to the input. Text placed beside the field is not announced with it.
WCAG: [3.3.1 Error Identification](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html)
### Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
| --- | --- | --- |
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Partially tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What each test covers:
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
What these statuses mean:
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
## Properties
### MonthpickerField
Also accepts all props from `HTMLAttributes`, `InputHTMLAttributes`.
- `id` (type `string`, required). Unique id for the input, used to link the label, hint and error to it. Also used as the base for related ids such as `{id}-error`.
- `error` (type `ReactNode`, optional). Error message shown below the input. When set, the input gets `aria-invalid` and an error border, and the message is linked to it with `aria-describedby`. Takes precedence over an error provided by a surrounding `Form`.
- `label` (type `string`, required). Visible label for the field. It is associated with the input, so it forms the input's accessible name.
- `name` (type `string`, optional). The `name` attribute passed to the input. Also used to look up the field's error from a surrounding `Form`.
- `hint` (type `ReactNode`, optional). Helper text shown below the label. It is part of the label element, so it is read as part of the input's accessible name.
- `optional` (type `boolean`, optional, default `false`). Marks the field as not required. This appends "(optional)" to the label (or, when the theme marks required fields, hides the required asterisk) and sets `aria-required` on the input only when the field is not optional.
- `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). Controls when the field stretches to the full width of its container. `always` is always full width, `never` always uses the width set by `characters`, and `small` is full width on small screens only.
- `characters` (type `number`, optional, default `8`). Width of the input, as an approximate number of characters (the width is set to `0.8em` per character, rounded up). Does not limit how much can be typed.
- `affixWidth` (type `number`, optional, default `40`). Extra width in pixels added to the width calculated from `characters`, to make room for icons or buttons inside the input.
- `iconLeft` (type `IconName | (string & {})`, optional). Name of an icon to show inside the left of the input. The input padding increases to make room for it.
- `iconRight` (type `IconName | (string & {})`, optional). Name of an icon to show inside the right of the input. The input padding increases to make room for it.
- `onChange` (type `(value: string) => void`, optional). Called with the new text when the user types (exactly as typed, not validated or reformatted) or picks a month from the picker (formatted `mm/yyyy`), and with an empty string when the month is cleared from the picker.
- `defaultValue` (type `string`, optional). Initial text of the input when uncontrolled, in `mm/yyyy` format.
- `defaultOpen` (type `boolean`, optional). Whether the month picker popover is open when first rendered.
- `isMonthUnavailable` (type `IsMonthUnavailable`, optional). Called for each month in the picker. Return `true` to make that month unavailable so it cannot be selected in the picker. It does not stop a month being typed into the input.