Skip to content

Recurrence Picker

Date and TimeSince 0.6.0
import { RecurrencePicker } from '@absoluteui/components/recurrence-picker';
import '@absoluteui/components/date-input/style.css';

Using Angular? See Recurrence Picker for Angular.

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.

  • Repeating meetings, reminders, reports, maintenance windows, or billing cycles.
  • Inside an event editor next to a start and end (Schedule Picker embeds it).
  • 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.

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) : '';
});
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;
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;
});
}

See section 33 and metadata/recurrence-picker/examples.json: weekly with preview, editing existing text, global configuration.

<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="...">
RecurrencePicker.create(host: HTMLElement, options: RecurrencePickerOptions): RecurrencePicker

Global configuration key: recurrencePicker (see Configuration).

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.
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.
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.
Event Detail Bubbles Cancelable Description
aui-change { value: RecurrenceRule | null; validity: RecurrencePickerValidity } Yes No The user changed the rule or its validity error.

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.
Slot Default icon
error alert-circle

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

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 }

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.

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)
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.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
2.5.8 Target Size (Minimum) AA
3.2.2 On Input A
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • 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.

Does not repeat, daily, weekly, monthly, yearly, custom (a rule the controls cannot show), disabled, required, invalid.

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)
  • 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.

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.

Follows direction; controls flow right to left; the embedded Date Picker mirrors.

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.

configureAbsoluteUI({ components: { recurrencePicker: { previewCount: 8, validationTiming: 'input' } } });

Global options: validationTiming, previewCount, plus inherited locale, timezone, disambiguation, direction.

Instance options override global and theme defaults; formControl is init-only; everything else is dynamic.

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.

All text through textContent; options come from Intl and messages. The form value is core serialization output.

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.

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.

One rule per instance.

  • 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.
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.

  • 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.
  • BYWEEKNO, BYYEARDAY, BYHOUR, BYMINUTE, BYSECOND, RSCALE.
  • TZID or UTC values in EXDATE/RDATE text (floating local and VALUE=DATE only).