Recurrence Picker
import { RecurrencePicker } from '@absoluteui/components/recurrence-picker';import '@absoluteui/components/date-input/style.css';Using Angular? See Recurrence Picker for Angular.
What it does
Section titled “What it does”Edits how something repeats. The value is a RecurrenceRule from @absoluteui/core: the RFC 5545 RRULE subset FREQ (daily, weekly, monthly, yearly), INTERVAL, COUNT, UNTIL, BYDAY (with ordinals for monthly and yearly), BYMONTHDAY, BYMONTH, BYSETPOS, and WKST, plus EXDATE and RDATE lists. Unsupported parts are refused by the core parser, never ignored.
The picker is a labelled fieldset of native controls: a frequency select (“Does not repeat”, Daily, Weekly, Monthly, Yearly), an interval, weekday toggle buttons, day-of-month or nth-weekday choices (with a month for yearly rules), and the end (never, on a date through an embedded Date Picker, or after a number of occurrences). A live summary describes the rule (“Every 2 weeks on Monday and Wednesday, until Dec 31, 2026”), and with a start the next occurrences are listed in the timezone. The form value is iCalendar text.
Core helpers for rules: recurrenceRule, parseRecurrenceRule, recurrenceRuleToString, expandRecurrence, describeRecurrence.
When to use
Section titled “When to use”- Repeating meetings, reminders, reports, maintenance windows, or billing cycles.
- Inside an event editor next to a start and end (Schedule Picker embeds it).
When not to use
Section titled “When not to use”- One-off dates: use Date Picker or Schedule Picker without recurrence.
- Hourly or minute-level repetition: not supported by the rule subset.
- Arbitrary lists of dates: store them as RDATE values in code or use a date list.
Examples
Section titled “Examples”Weekly on the start weekday, with a preview
Section titled “Weekly on the start weekday, with a preview”import { plainDate, plainDateTime, plainTime, recurrenceRuleToString } from '@absoluteui/core';import { RecurrencePicker, type RecurrencePickerChangeDetail } from '@absoluteui/components/recurrence-picker';
const host = document.createElement('div');document.body.append(host);RecurrencePicker.create(host, { label: 'Repeat', name: 'rrule', start: plainDateTime(plainDate(2026, 9, 29), plainTime(9)), timezone: 'Europe/London',});host.addEventListener('aui-change', (event) => { const { value } = (event as CustomEvent<RecurrencePickerChangeDetail>).detail; host.dataset.rule = value ? recurrenceRuleToString(value) : '';});Edit an existing rule from iCalendar text
Section titled “Edit an existing rule from iCalendar text”import { parseRecurrenceRule, plainDate } from '@absoluteui/core';import { RecurrencePicker } from '@absoluteui/components/recurrence-picker';
const host = document.createElement('div');document.body.append(host);const picker = RecurrencePicker.create(host, { label: 'Billing cycle', value: parseRecurrenceRule('FREQ=MONTHLY;BYDAY=-1FR;COUNT=12'), start: plainDate(2026, 1, 30), frequencies: ['monthly', 'yearly'], previewCount: 3,});export const text = picker.summary;Global preview size
Section titled “Global preview size”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { recurrencePicker: { previewCount: 8, validationTiming: 'input' } } });import { plainDate, plainDateTime, plainTime } from '@absoluteui/core';import { RecurrencePicker, type RecurrencePickerChangeDetail } from '@absoluteui/components/recurrence-picker';import '@absoluteui/components/date-input/style.css';import '@absoluteui/components/calendar/style.css';import '@absoluteui/components/date-picker/style.css';import '@absoluteui/components/recurrence-picker/style.css';
const host = document.querySelector<HTMLElement>('#repeat');if (host) { const picker = RecurrencePicker.create(host, { label: 'Repeat', name: 'rrule', start: plainDateTime(plainDate(2026, 9, 29), plainTime(9)), timezone: 'Europe/London' }); host.addEventListener('aui-change', (event) => { const { validity } = (event as CustomEvent<RecurrencePickerChangeDetail>).detail; host.dataset.valid = String(validity.valid); host.title = picker.summary; });}More usage
Section titled “More usage”See section 33 and metadata/recurrence-picker/examples.json: weekly with preview, editing existing text, global configuration.
Anatomy
Section titled “Anatomy”<div class="aui-field aui-recurrence-picker [--invalid] [--disabled]"> (consumer host) <fieldset class="aui-recurrence-picker__fieldset" aria-describedby="{help} {error} {summary}"> <legend class="aui-field__label">Repeat</legend> <div class="aui-recurrence-picker__row --frequency"><label>Repeats</label><select class="…__frequency"> <div class="aui-recurrence-picker__row --interval"><label>Every</label><input type="number" class="…__number"><span class="…__unit">weeks</span> <div class="aui-recurrence-picker__row --weekdays" role="group" aria-label="Repeat on"> <button class="aui-recurrence-picker__weekday" aria-pressed="true" aria-label="Monday">M</button>... <fieldset class="aui-recurrence-picker__row --month"><legend>Repeat on</legend> <div class="…__choice"><label><input type="radio"> On day</label> [month select] <select aria-label="Day of month"> <div class="…__choice"><label><input type="radio"> On the</label> <select aria-label="Week of the month"> <select aria-label="Weekday"> [month select] <fieldset class="aui-recurrence-picker__row --end"><legend>Ends</legend> Never | On a date [Date Picker "End date"] | After [number] occurrences <p class="aui-recurrence-picker__summary" aria-live="polite">Every week on Tuesday</p> <div class="aui-recurrence-picker__preview"><p>Next occurrences</p><ul>... <div class="aui-field__description"> <div class="aui-field__error"> <input type="hidden" name="...">API reference
Section titled “API reference”Factory
Section titled “Factory”RecurrencePicker.create(host: HTMLElement, options: RecurrencePickerOptions): RecurrencePickerGlobal configuration key: recurrencePicker (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Legend of the fieldset (required accessible name). Required. | |
hideLabel |
boolean |
false |
Visually hide the legend; it stays the accessible name. |
value |
RecurrenceRule | null |
The rule; null means “Does not repeat”. Also the value restored by reset(). | |
start |
PlainDate | PlainDateTime | null |
Series start: defaults, the minimum end date, and the preview. | |
frequencies |
readonly ('daily' | 'weekly' | 'monthly' | 'yearly')[] |
Frequencies offered. Default all four. | |
maxInterval |
number |
99 |
Largest interval. |
maxCount |
number |
999 |
Largest occurrence count. |
name |
string |
Form field name of the hidden input (iCalendar text). | |
description |
string |
Help text referenced by the fieldset. | |
required |
boolean |
false |
“Does not repeat” is invalid. |
disabled |
boolean |
false |
Disables every control; not submitted. |
firstDayOfWeek |
IsoWeekday |
First weekday toggle. Default: the locale’s first day of week. | |
validate |
(value: RecurrenceRule | null) => string | null |
Consumer validation after built-in checks. | |
formControl |
boolean |
true |
Advanced, read at creation: false skips registration with Form (Schedule Picker registers itself). Set at creation only. |
icons |
Partial<{ error: IconRef }> |
Icon slot overrides. | |
validationTiming |
'blur' | 'input' | 'submit' |
"blur" |
When validation messages first appear. Globally configurable. |
previewCount |
number |
5 |
Next occurrences listed under the summary (needs start); 0 hides the preview. Globally configurable. |
locale |
string |
"en-US" |
Month and weekday names, dates, numbers, the first day of the week, and messages. Globally configurable. Inherited from common configuration. |
timezone |
string |
"UTC" |
Timezone of the start wall-clock time, the preview, and UNTIL. Globally configurable. Inherited from common configuration. |
disambiguation |
'compatible' | 'earlier' | 'later' | 'reject' |
"compatible" |
DST policy for wall-clock times in a gap or overlap. Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
Reading direction; Left/Right arrow keys and chevrons follow it. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Consumer host. |
fieldset |
HTMLFieldSetElement |
The labelled fieldset holding every control. |
untilPicker |
DatePicker |
The end-date Date Picker (not registered with Form). |
value |
RecurrenceRule | null |
Current rule; null when not repeating or invalid (a custom error keeps the value). |
validity |
RecurrencePickerValidity |
{ valid, error, message }. |
summary |
string |
Localized description of the current rule. |
name |
string | null |
Form field name. |
label |
string |
Legend text. |
disabled |
boolean |
Disabled state. |
dirty |
boolean |
The rule text differs from the initial value. |
touched |
boolean |
Focus has left the control at least once. |
options |
Readonly<RecurrencePickerOptions> |
Current options. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
setValue(value: RecurrenceRule | null): void |
Programmatic value (no events), kept exactly; clears an external custom error. |
update(options: Partial<RecurrencePickerOptions>): void |
Merge and apply options. |
reportValidity(): boolean |
Show messages now and return validity. |
setCustomError(message: string | null): void |
External error; cleared when the user edits. |
getFormValue(): string | null |
iCalendar text, or null when not repeating or invalid. |
focus(): void |
Focus the frequency select. |
reset(): void |
Restore the initial value and clear state (no events). |
destroy(): void |
Remove rendering, the inner Date Picker, listeners, and registration. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-change |
{ value: RecurrenceRule | null; validity: RecurrencePickerValidity } |
Yes | No | The user changed the rule or its validity error. |
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-recurrence-picker | --invalid | --disabled |
Host and state modifiers. |
aui-recurrence-picker__fieldset | __row | __row--frequency | --interval | --weekdays | --month | --end |
Fieldset and rows. |
aui-recurrence-picker__select | __select--month-day | --nth | --weekday | --month | __frequency | __number | __unit |
Native controls. |
aui-recurrence-picker__weekday |
Weekday toggle buttons (aria-pressed). |
aui-recurrence-picker__choice | __radio | __radio-label | __legend | __label | __until |
Radio choices with inline controls. |
aui-recurrence-picker__summary | __preview | __preview-heading | __preview-list |
Summary (live) and next occurrences. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
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-border-width-strong | --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-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --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-weight-medium | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-pill | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-4 | --aui-touch-target-min
TypeScript definitions
Section titled “TypeScript definitions”type RecurrenceFrequency = 'daily' | 'weekly' | 'monthly' | 'yearly';interface RecurrenceWeekday { weekday: IsoWeekday; nth?: number }interface RecurrenceRule { kind: 'RecurrenceRule'; frequency: RecurrenceFrequency; interval: number; count: number | null; until: PlainDate | PlainDateTime | Instant | null; byDay: readonly RecurrenceWeekday[]; byMonthDay: readonly number[]; byMonth: readonly number[]; bySetPos: readonly number[]; weekStart: IsoWeekday; exclusions: readonly (PlainDate | PlainDateTime)[]; inclusions: readonly (PlainDate | PlainDateTime)[] }type RecurrencePickerError = 'value-missing' | 'bad-interval' | 'no-weekday' | 'bad-count' | 'no-until' | 'bad-until' | 'custom';interface RecurrencePickerValidity { valid: boolean; error: RecurrencePickerError | null; message: string }interface RecurrencePickerChangeDetail { value: RecurrenceRule | null; validity: RecurrencePickerValidity }Accessibility
Section titled “Accessibility”Pattern: A fieldset of native form controls (select, number inputs, radio groups, toggle buttons) with a legend, a live summary, and a list of next occurrences.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Picker | Moves through the frequency select, interval, weekday toggles, radios, inline selects, the end date, and the count. |
| Space / Enter | Weekday toggle | Toggles the day. |
| Arrow keys | Radio group | Native radio selection (the inline controls follow). |
| ArrowUp / ArrowDown | Interval or count | Native number stepping. |
| Element | Role |
|---|---|
| fieldset | group (native) |
| weekday toggles | button with aria-pressed inside role=group |
| monthly and end choices | radio groups in nested fieldsets |
| summary | none (aria-live=polite) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-describedby |
fieldset | Help text, error message, and the summary |
aria-pressed |
weekday toggle | “true” when the day repeats |
aria-label |
weekday toggle | Full weekday name (the visible text is the narrow name) |
aria-label |
inline selects | “Day of month”, “Week of the month”, “Weekday”, “Month”, “Number of occurrences” |
aria-invalid |
interval and count inputs | “true” while their error is shown |
Every control is native and in document order; hidden rows (for other frequencies) are not focusable. Rebuilding options keeps the selected values.
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 summary is a polite live region, so each change is read as a sentence (“Every 2 weeks on Monday and Wednesday”).
- Radio groups are grouped by name but have no form owner, so only the hidden input is submitted.
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Behavior
Section titled “Behavior”States
Section titled “States”Does not repeat, daily, weekly, monthly, yearly, custom (a rule the controls cannot show), disabled, required, invalid.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| does not repeat | frequency select | that frequency with defaults from start (weekday, day, month) |
| any frequency | weekday toggle, interval, radios, selects | new rule; summary and preview update; aui-change |
| custom | another frequency | a rule built from the controls (custom is dropped) |
| end never | “On a date” + date / “After” + count | UNTIL / COUNT |
| any | invalid interval, count, date, or no weekday | invalid (value null) |
Accessibility notes
Section titled “Accessibility notes”- Controls are native, so browser and assistive-technology support is broad.
- Pressed weekdays differ by fill and border weight, not color alone.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
Responsive behavior
Section titled “Responsive behavior”Rows wrap; each radio keeps its inline controls on one line when they fit. Reflows at 320 px. Weekday toggles meet the 44 px target size.
Right-to-left
Section titled “Right-to-left”Follows direction; controls flow right to left; the embedded Date Picker mirrors.
Theming
Section titled “Theming”Shared control surface for selects and numbers, primary fill for pressed weekdays, muted text for units and the preview. Forced colors: Highlight for pressed days, GrayText for disabled.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { recurrencePicker: { previewCount: 8, validationTiming: 'input' } } });Global options: validationTiming, previewCount, plus inherited locale, timezone, disambiguation, direction.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults; formControl is init-only; everything else is dynamic.
Timezones
Section titled “Timezones”A rule has no start or zone. With a timed start (PlainDateTime, read in timezone), the preview expands occurrences at the start’s wall-clock time in that zone across DST changes (a start in a skipped hour moves forward by the gap, the RFC 5545 rule); an end date becomes UNTIL at the last second of that day in the timezone, as UTC (UNTIL=20261031T235959Z), as RFC 5545 requires for zoned starts. With an all-day start (PlainDate), UNTIL stays a date. Without timezone, the configured timezone (UTC by default) is used.
error (alert-circle). The embedded Date Picker uses its own calendar icon.
Security
Section titled “Security”All text through textContent; options come from Intl and messages. The form value is core serialization output.
Data handling
Section titled “Data handling”No persistence or network access. One hidden input carries the iCalendar text (RRULE:... plus EXDATE/RDATE lines, \n-separated); radios have no form owner, so nothing else is submitted.
Performance
Section titled “Performance”About 60 native elements per instance; previews expand at most 20 occurrences. Budget (e2e): create with a start and a 5-occurrence preview < 15 ms per instance.
Supported data scale
Section titled “Supported data scale”One rule per instance.
Edge cases
Section titled “Edge cases”- Monthly “on day 31” skips shorter months (RFC 5545: invalid dates are skipped); “last day” (
BYMONTHDAY=-1) covers every month. - Yearly on 29 February occurs in leap years only.
- A start that is the fifth weekday of its month defaults to “the fifth”; choose “last” for months with four.
- Removing every weekday of a weekly rule is invalid (“Choose at least one day.”).
- An existing COUNT and UNTIL together are refused by the core (RFC 5545).
- EXDATE/RDATE and WKST of the value survive edits.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
RECURRENCE_PICKER_OPTIONS |
configuration | Empty label, invalid value, start, frequencies, bounds, firstDayOfWeek, callbacks, or config. |
RECURRENCE_INVALID |
validation | recurrenceRule() with contradictory or out-of-range parts. |
RECURRENCE_EXPAND |
validation | expandRecurrence() with bounds of the wrong kind or an invalid limit. |
Validation errors in the UI: value-missing, bad-interval, no-weekday, bad-count, no-until, bad-until, custom.
Known limitations
Section titled “Known limitations”- One day of month or one nth weekday per month in the controls; other rules show as “Custom”.
- No hourly or minute frequencies; EXDATE/RDATE are not edited here.
- The end is a date, not a time.
- Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- BYWEEKNO, BYYEARDAY, BYHOUR, BYMINUTE, BYSECOND, RSCALE.
TZIDor UTC values in EXDATE/RDATE text (floating local andVALUE=DATEonly).