Year Picker
import { YearPicker } from '@absoluteui/components/year-picker';import '@absoluteui/components/year-picker/style.css';Using Angular? See Year Picker for Angular.
What it does
Section titled “What it does”Entry of a calendar year. YearPicker renders one four-digit year segment inside a labelled group, plus a “Choose year” button that opens a non-modal dialog popup with a grid of the ten years of a decade (paged by decade). The value is a plain number (an integer from 1 to 9999); a hidden input submits it as four zero-padded digits (YYYY). It shares its engine with Month and Quarter Picker; this page states every behavior in full, and year-specific details are marked.
When to use
Section titled “When to use”- A calendar year on its own: a model year, a graduation year, a tax or reporting year.
- Users should be able to type a year quickly and also browse nearby years.
- Year fields with bounds or unavailable years.
When not to use
Section titled “When not to use”- A month or a specific day: use Month Picker, Date Picker, or Date Input.
- A calendar quarter: use Quarter Picker.
- A fiscal year that does not start in January: the value is a calendar year; label the field accordingly or store the fiscal mapping yourself.
- An arbitrary number that is not a year: use Number Input.
Examples
Section titled “Examples”Required year with bounds
Section titled “Required year with bounds”import { YearPicker } from '@absoluteui/components/year-picker';
const host = document.createElement('div');document.body.append(host);const picker = YearPicker.create(host, { label: 'Model year', name: 'modelYear', required: true, min: 1990, max: 2027,});host.addEventListener('aui-change', () => { host.dataset.valid = String(picker.validity.valid);});Unavailable years and the change detail
Section titled “Unavailable years and the change detail”import { YearPicker, type YearPickerChangeDetail } from '@absoluteui/components/year-picker';
const closedYears = new Set([2020, 2021]);const host = document.createElement('div');document.body.append(host);YearPicker.create(host, { label: 'Tax year', value: 2025, isYearDisabled: (year) => closedYears.has(year), validate: (year) => (year !== null && year > 2026 ? 'Future years cannot be filed.' : null),});host.addEventListener('aui-change', (event) => { const { value, validity } = (event as CustomEvent<YearPickerChangeDetail>).detail; host.dataset.year = value === null ? '' : String(value); host.dataset.error = validity.error ?? '';});Global year picker defaults
Section titled “Global year picker defaults”import { configureAbsoluteUI } from '@absoluteui/core';import '@absoluteui/components/year-picker';
configureAbsoluteUI({ components: { yearPicker: { closeOnSelect: false, validationTiming: 'input' } } });import { YearPicker, type YearPickerChangeDetail } from '@absoluteui/components/year-picker';import '@absoluteui/components/year-picker/style.css';
const host = document.querySelector<HTMLElement>('#graduation');if (host) { YearPicker.create(host, { label: 'Graduation year', name: 'graduation', max: 2030 }); host.addEventListener('aui-change', (event) => { const { value } = (event as CustomEvent<YearPickerChangeDetail>).detail; host.dataset.year = value === null ? '' : String(value); });}More usage
Section titled “More usage”import { YearPicker } from '@absoluteui/components/year-picker';
const host = document.createElement('div');document.body.append(host);YearPicker.create(host, { label: 'Model year', name: 'modelYear', required: true, min: 1990, max: 2027 });Anatomy
Section titled “Anatomy”<div class="aui-field aui-period-picker aui-year-picker" data-aui-control> (consumer host; registered with Form) <label class="aui-field__label">Label<span class="aui-field__required" aria-hidden="true">*</span></label> <div class="aui-year-picker__group aui-period-picker__group" role="group" aria-labelledby=label aria-describedby> <div class="aui-period-picker__fields"> <input class="aui-period-picker__segment aui-period-picker__segment--year" aria-label="Year" placeholder="yyyy"> </div> <button class="aui-period-picker__button" aria-haspopup="dialog" aria-expanded aria-controls=popup aria-label="Choose year"> </div> <div class="aui-field__description"> (hidden when empty) <div class="aui-field__error aui-validation-message"> (hidden when no message is shown) <input type="hidden" name> <div class="aui-period-picker__popup" role="dialog" aria-modal="false" aria-label="Choose year" popover="manual"> <div class="aui-period-picker__header"> <button class="aui-period-picker__nav aui-period-picker__prev" aria-label="Previous 10 years"> <div class="aui-period-picker__heading" aria-live="polite">2020 - 2029</div> <button class="aui-period-picker__nav aui-period-picker__next" aria-label="Next 10 years"> </div> <div class="aui-period-picker__grid aui-year-picker__grid" role="grid" aria-labelledby=heading> <div class="aui-period-picker__row" role="row"> (2 rows of 5) <div class="aui-period-picker__cell" role="gridcell" aria-label="2020" tabindex="-1"> <span class="aui-period-picker__cell-text">2020</span> </div> ... </div> </div> </div></div>The popup sits inside the host in the DOM (it renders in the top layer), so moving focus between the segment, the button, and the popup does not mark the field touched.
API reference
Section titled “API reference”Factory
Section titled “Factory”YearPicker.create(host: HTMLElement, options: YearPickerOptions): YearPickerGlobal configuration key: yearPicker (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Visible label and accessible name of the group (via aria-labelledby). Must be a non-empty string. Required. | |
hideLabel |
boolean |
false |
Visually hide the label; it remains the accessible name. |
value |
number | null |
null |
Value (integer year 1-9999). The value passed to create() is the value restored by reset(). |
min |
number | null |
null |
Inclusive lower bound (integer year; validation and popup). Must not be after max. |
max |
number | null |
null |
Inclusive upper bound (integer year; validation and popup). |
name |
string |
Form field name; a hidden input carries the year as zero-padded YYYY. | |
description |
string |
Help text (text only) referenced by the group’s aria-describedby. | |
required |
boolean |
false |
An empty entry is invalid (value-missing); the segment is required and the required indicator is shown. |
disabled |
boolean |
false |
Disables the segment and the button; the popup does not open and the value is not submitted. |
readOnly |
boolean |
false |
Focusable and submitted, but not editable; the popup does not open. |
validate |
(value: number | null) => string | null |
Consumer validation after the built-in checks; return a non-empty message to mark the value invalid (error ‘custom’). | |
isYearDisabled |
(year: number) => boolean |
Return true to disable a year: focusable in the popup but not selectable; typed, it is invalid (error ‘unavailable’). | |
today |
PlainDate |
Advanced: fixed today instead of the current date in timezone; it decides the current year. For tests, server rendering, and demos with a fixed clock. | |
icons |
Partial<YearPickerIcons> |
Icon slot overrides: calendar, previous, next, error. | |
validationTiming |
'blur' | 'input' | 'submit' |
"blur" |
When validation messages first appear; afterwards they update live. Globally configurable. |
closeOnSelect |
boolean |
true |
Close the popup after a year is picked. Globally configurable. |
locale |
string |
"en-US" |
Year formatting, headings, labels, and messages. Globally configurable. Inherited from common configuration. |
timezone |
string (IANA) |
"UTC" |
Zone used only to determine the current year (aria-current, default focus, and the start of stepping an empty segment). Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
Popup and grid direction: grid arrow-key mirroring, chevrons, and popup alignment (applied when the popup opens). Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Consumer host; its children are owned by the component. Registered with Form. |
group |
HTMLDivElement |
The labelled role=group with the segment and the button; control boundary and popup anchor. |
button |
HTMLButtonElement |
Opens the popup (aria-haspopup=dialog). |
popup |
HTMLDivElement |
role=dialog aria-modal=false popup (popover=manual). |
grid |
HTMLDivElement |
The role=grid of years inside the popup. |
value |
number | null |
Current value; null when empty, incomplete, or failing a built-in check. |
validity |
YearPickerValidity |
{ valid, error, message }; recomputed on every change. |
isOpen |
boolean |
Whether the popup is open. |
name |
string | null |
Form field name. |
label |
string |
Label text. |
disabled |
boolean |
Disabled state. |
dirty |
boolean |
Value differs from the initial value, or the segment holds an entry that does not parse to a value. |
touched |
boolean |
Focus has left the whole control (segment, button, and popup) at least once. |
options |
Readonly<YearPickerOptions> |
Current instance options. |
focusedValue |
number |
The year holding the roving tab stop in the popup grid. |
current |
number |
The year containing today in the configured timezone (or the today option). |
Methods
Section titled “Methods”| Method | Description |
|---|---|
setValue(value: number | null): void |
Programmatic value (no events); clears an external custom error. Throws YEAR_PICKER_OPTIONS for a value that is not an integer 1-9999. |
update(options: Partial<YearPickerOptions>): void |
Merge, validate, and apply options. Closes an open popup when it becomes disabled or read-only. |
open(): void |
Open the popup and focus the selected year (or the current year), clamped to min/max. No-op when open, disabled, or read-only. |
close(): void |
Close the popup; focus inside it returns to the button. |
reportValidity(): boolean |
Show messages now (for every validation timing) and return validity. |
setCustomError(message: string | null): void |
External error (server, cross-field rule); null or ‘’ clears it; cleared when the user edits. |
getFormValue(): string | null |
Zero-padded YYYY, or null when the value is null. |
focus(): void |
Focus the year segment. |
reset(): void |
Close, restore the initial value, and clear dirty/touched/submitted state (no events). |
destroy(): void |
Close, remove rendering, host classes, listeners, form registration, and subscriptions. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-change |
{ value: number | null; validity: YearPickerValidity } |
Yes | No | The user changed the value or its validity error (typing, stepping, or picking a year). |
aui-open |
null |
No | No | The popup opened. |
aui-close |
null |
No | No | The popup closed. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-field aui-period-picker aui-year-picker |
Host. |
aui-period-picker--invalid | --disabled | --readonly |
State modifiers on the host. |
aui-field__label | __required | __description | __error |
Shared field chrome (the error is also aui-validation-message). |
aui-year-picker__group aui-period-picker__group |
Labelled group; control boundary, focus indicator, and popup anchor. |
aui-period-picker__fields |
LTR-isolated container of the segment. |
aui-period-picker__segment | __segment--year |
Year segment input. |
aui-period-picker__button |
Choose year button. |
aui-period-picker__popup |
Top-layer dialog popup. |
aui-period-picker__header | __heading | __nav | __prev | __next |
Popup header: decade heading (aria-live) and previous/next 10 years buttons. |
aui-period-picker__grid aui-year-picker__grid |
Grid of 10 years (2 rows of 5). |
aui-period-picker__row |
Grid row. |
aui-period-picker__cell | --selected | --current | --disabled | --empty |
Year cell and its states; –empty is the year-0 slot on the first page. |
aui-period-picker__cell-text |
Year text in a cell. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
calendar |
calendar |
previous |
chevron-left |
next |
chevron-right |
error |
alert-circle |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-on-primary | --aui-color-primary | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-height-sm | --aui-control-padding-x | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-bold | --aui-font-weight-medium | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3
TypeScript definitions
Section titled “TypeScript definitions”interface YearPickerConfig { validationTiming: ValidationTiming; closeOnSelect: boolean }type ValidationTiming = 'blur' | 'input' | 'submit';type YearPickerError = 'incomplete' | 'invalid' | 'value-missing' | 'range-underflow' | 'range-overflow' | 'unavailable' | 'custom';interface YearPickerValidity { readonly valid: boolean; readonly error: YearPickerError | null; readonly message: string }interface YearPickerChangeDetail { value: number | null; validity: YearPickerValidity }interface YearPickerIcons { calendar: IconRef; previous: IconRef; next: IconRef; error: IconRef }Accessibility
Section titled “Accessibility”Pattern: Single-segment year field in a labelled group with a button that opens a non-modal dialog popup (WAI-ARIA APG date picker dialog pattern, non-modal variant) containing a paged role=grid of years with a roving tabindex.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Digits | Year segment | Enters up to four digits; fewer than four digits is incomplete (year 5 is typed 0005). Non-digits are removed. |
| ArrowUp / ArrowDown | Year segment (not read-only) | Increments or decrements the year, stopping at 1 and 9999; an empty segment starts at the current year. |
| Alt+ArrowDown | Year segment | Opens the popup without changing the segment. |
| Tab / Shift+Tab | Group | Moves from the segment to the button. |
| Enter / Space | Button | Opens the popup (or closes it when open); focus moves to the selected year, else the current year, clamped to min/max. |
| ArrowRight / ArrowLeft | Year cell | Next / previous year (mirrored in RTL), crossing into the adjacent decade. |
| ArrowDown / ArrowUp | Year cell | Plus / minus 5 years (next / previous row), crossing into the adjacent decade. |
| Home / End | Year cell | First / last year of the row. |
| PageDown / PageUp | Year cell | Plus / minus 10 years (next / previous decade). |
| Shift+PageDown / Shift+PageUp | Year cell | Plus / minus 100 years. |
| Enter / Space | Year cell | Picks the year unless it is disabled; with closeOnSelect (default) the popup closes and focus returns to the button, otherwise focus stays on the cell. Emits aui-change if the value or validity changed. |
| Enter / Space | Previous / next 10 years button | Shows the previous / next decade; no-op while aria-disabled. |
| Escape | Popup open | Closes the popup without changing the value; focus inside the popup returns to the button. Only the topmost overlay responds, so a surrounding Dialog stays open. |
| Tab / Shift+Tab | Popup | Moves between the previous and next buttons and the focused year; focus leaving the popup (other than to the choose button) closes it. |
| Element | Role |
|---|---|
| segment container | group |
| year segment (input type=text, inputmode=numeric) | textbox (native) |
| choose year button | button (native), aria-haspopup=dialog |
| popup | dialog (aria-modal=false) |
| previous / next 10 years buttons | button (native) |
| year grid | grid |
| grid row | row |
| year cell | gridcell |
| icons | none (aria-hidden=true) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-labelledby |
group | Label id |
aria-describedby |
group | Help text and visible error message ids |
aria-label |
segment | Localized “Year” |
aria-invalid |
segment | “true” only while an error message is shown |
aria-label |
button | “Choose year”, or “Change year, {value}” (for example “Change year, 2026”) when a value is set |
aria-haspopup |
button | dialog |
aria-expanded |
button | “true” while the popup is open |
aria-controls |
button | Popup id |
aria-label |
popup | Localized “Choose year” |
aria-modal |
popup | “false” (non-modal; the page stays reachable) |
aria-live |
heading | polite (announces the decade, for example “2020 - 2029”, when the page changes) |
aria-label |
previous / next buttons | “Previous 10 years” / “Next 10 years” |
aria-disabled |
previous / next buttons | “true” when the page reaches min/max or year 1/9999 (focus stays on the button) |
aria-labelledby |
grid | Heading id |
aria-label |
cell | Formatted year, for example “2026” (absent on the empty year-0 slot) |
aria-selected |
cell | “true” on the selected year only (absent otherwise) |
aria-current |
cell | “date” on the current year (configured timezone or today option) |
aria-disabled |
cell | “true” for years outside min/max or rejected by isYearDisabled |
The segment and the button are tab stops. Opening moves focus to one year cell (roving tabindex: exactly one cell has tabindex=0; the empty year-0 slot is never focusable). Movement is clamped to min/max and years 1-9999; disabled years stay focusable. Picking with closeOnSelect, Escape, and closing through the button return focus to the button; focus is moved before the popup hides, so it never falls to the document body. A pointer press outside the popup and the button closes it. Moving focus between the segment, button, and popup does not mark the field touched.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- The label is a <label> associated through aria-labelledby because a group is not labelable; clicking the label focuses the segment.
- The required asterisk is aria-hidden; the segment carries the native required state.
- The segment remains the primary input; the grid is an alternative, and a typed year rejected by isYearDisabled gets a localized message so both paths agree.
- The popup is non-modal: focus is not trapped and the rest of the page stays reachable.
- Errors appear after focus leaves the whole control by default.
- Screen-reader behavior (dialog announcement, page changes, cell states) has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Behavior
Section titled “Behavior”States
Section titled “States”empty, segment focused, partial (incomplete), valid, invalid (message shown), open, cell focused, cell selected, current year, cell disabled, disabled, read-only, required.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| empty | user types fewer than four digits | dirty, incomplete; message shown only with validationTiming: 'input' |
| incomplete | fourth digit typed | valid, or invalid / range-underflow / range-overflow / unavailable / custom; aui-change |
| closed | button, Alt+ArrowDown, or open() |
open; focus on the selected (else current) year; aui-open |
| open | Enter, Space, or click on an enabled year | value set, aui-change if changed; with closeOnSelect closed, focus on the button, aui-close |
| open | Escape, the button, or close() |
closed; value kept; focus inside the popup returns to the button; aui-close |
| open | Tab out of the popup, or a pointer press outside the popup and button | closed; focus moves on |
| any | focus leaves the host | touched; messages shown (blur timing) |
| any | reportValidity() |
submitted; messages shown for every timing |
| invalid (custom error) | user edits | custom error cleared, validity recomputed |
| any | reset() |
initial value; pristine, untouched, closed |
Accessibility notes
Section titled “Accessibility notes”- The segment stays the primary input; the grid is an alternative. A typed year rejected by
isYearDisabledgets a localized message, so both paths agree. - The popup is non-modal: focus is not trapped. Escape and outside presses close it through the shared overlay stack, so only the topmost overlay responds.
- Focus is moved before the popup hides, so it never falls to the document body.
- Errors appear after the whole control loses focus. The label focuses the segment when clicked; the required asterisk is
aria-hidden. - Manual screen-reader verification is pending (see Known limitations).
Responsive behavior
Section titled “Responsive behavior”The group hugs its segment and button, up to 100% of the container. The popup is positioned under the group (bottom-start), is at most the viewport width minus a gutter, caps its height to the available space (--aui-available-height), and scrolls when taller.
Right-to-left
Section titled “Right-to-left”The year segment renders as an LTR-isolated run. The button sits at the inline end. The popup and grid take direction: grid ArrowLeft/ArrowRight are mirrored, the chevrons are flipped, and the popup aligns to the inline start of the group.
Theming
Section titled “Theming”Role tokens only. The focused segment uses color-selected / color-selected-text; the selected year uses color-primary / color-on-primary; the current year is bold with an inset color-border-strong border; unavailable years are muted and struck through. In forced-colors mode the focused segment and the selected year use Highlight / HighlightText, the current year gets a CanvasText outline, disabled cells and buttons use GrayText, and the popup has a CanvasText border.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { yearPicker: { closeOnSelect: false, validationTiming: 'input' } } });Changes apply to existing instances immediately.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic (update()); there are no init-only options.
Timezones
Section titled “Timezones”Values are plain year numbers and never shift. The configured timezone (default UTC, never the host zone) only decides the current year: the cell marked aria-current="date", the year focused when the popup opens on an empty field, and the starting year for ArrowUp/ArrowDown in an empty segment. At 2026-12-31 19:00 UTC the current year is 2026 in UTC and America/New_York, but 2027 with timezone: 'Asia/Kolkata'. The advanced today option pins today to a fixed PlainDate.
icons.calendar (default calendar), icons.previous / icons.next (defaults chevron-left / chevron-right), and icons.error (default alert-circle).
Security
Section titled “Security”All text uses textContent and Intl formatting; no HTML parsing. Validation messages never include the entered value; range messages include only the formatted bounds. CSP-safe: no inline handlers; positioning writes styles through the CSSOM.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. When name is set, a hidden input carries the zero-padded YYYY; it is empty while the entry is empty or fails a built-in check, and disabled (not submitted) while the picker is disabled. A read-only picker is submitted.
Performance
Section titled “Performance”The popup and its 10 cells are created once per picker; opening and paging only re-render cell text and state. Listeners are delegated; one configuration subscription per instance. Budget: under 5 ms average per YearPicker.create across 100 instances in headless Chromium.
Supported data scale
Section titled “Supported data scale”100 pickers per page (performance test); years 1 to 9999.
Edge cases
Section titled “Edge cases”- Decade pages start at multiples of ten; the first page shows years 1 to 9 (heading “1 - 9”) with an empty slot where year 0 would be, and its previous button is
aria-disabled. 0000reportsinvalid(“Enter a valid year.”);26reportsincomplete(“Enter the year with 4 digits.”).- Year 5 is shown and submitted as
0005; ArrowUp from year 1 in the grid stays on year 1. - Built-in errors set
valuetonull; acustomerror keeps the value. update({ value })changes the value but not the value restored byreset()(thevaluepassed tocreate()).- Inside a modal Dialog, the first Escape closes the popup and the second closes the dialog.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
YEAR_PICKER_OPTIONS |
configuration | Missing or blank label; value/min/max not an integer 1-9999 (for example 2026.5 or 0); min after max; non-function validate or isYearDisabled; today not a PlainDate. Details name the property (for example yearPicker.value). |
CFG_INVALID_VALUE |
configuration | Invalid validationTiming, closeOnSelect, locale, timezone, or direction. |
Validation (validity.error) |
validation | incomplete, invalid, value-missing, range-underflow, range-overflow, unavailable, custom. |
Built-in checks run in that order: empty (value-missing when required), four-digit year (incomplete), range 1-9999 (invalid), min, max, isYearDisabled (unavailable), then validate, then an external custom error. English messages: “This field is required.”, “Enter the year with 4 digits.”, “Enter a valid year.”, “Enter {min} or later.”, “Enter {max} or earlier.”, “This year is not available.”. A failed construction leaves nothing behind.
Known limitations
Section titled “Known limitations”- Years 1 to 9999 only; no year 0, negative years, or eras.
- The year must be typed with four digits; only ASCII digits are accepted.
- Calendar years only; fiscal years with a non-January start are not modeled.
- Single years only; no Cancel/OK buttons (picking commits).
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- HTML in the label, help text, cells, or messages.
- Non-Gregorian calendars or eras.
- Custom cell content or grid layouts.