Recurring Events
Give an event a rule and it repeats across every view. The rule is a subset of RFC 5545, stored on the event itself; occurrences are computed for the range on screen and nowhere else.
A Rule on an Event
An event with recurrence is a series. Its start and end are the first occurrence and set the time of day and the length of every one after it. The array
holds one event; the views show as many as fall in the visible range.
// One event with a rule is a series. What the views show are its occurrences,
// computed for the visible range only, so a rule with no end costs nothing.
const yoga: EventInput = {
id: 'yoga',
title: 'Yoga',
start: '2026-09-15T07:00',
end: '2026-09-15T08:00',
recurrence: { freq: 'weekly', byDay: [2, 4], count: 6 } // Tue and Thu, six classes
};Rules in This Calendar
Seven series and nothing else. Switch between month, week and agenda to follow each one.
October 2026
Daily standup
Every weekday
{ freq: 'weekly', byDay: [1, 2, 3, 4, 5] }Sprint demo
Every other Friday
{ freq: 'weekly', interval: 2, byDay: [5] }Budget review
The same day each month
{ freq: 'monthly' }Yoga class
Tuesdays and Thursdays, six times
{ freq: 'weekly', byDay: [2, 4], count: 6 }Mentoring
Wednesdays until three weeks from now
{ freq: 'weekly', byDay: [3], until }Team lunch
Thursdays, except one
{ freq: 'weekly', byDay: [4], exDates }Company anniversary
Once a year, all day
{ freq: 'yearly' }// Every day, or every other day
{ freq: 'daily' }
{ freq: 'daily', interval: 2 }
// Weekly on the weekday of start; or on the days listed (0 is Sunday).
// A week is counted from weekStart, else the scheduler's weekStartsOn.
{ freq: 'weekly' }
{ freq: 'weekly', byDay: [1, 3, 5] }
{ freq: 'weekly', interval: 2, byDay: [5], weekStart: 1 } // every other Friday
// Monthly on the day of the month start falls on. A month without that day is
// skipped: a series from the 31st runs in January, March, May...
{ freq: 'monthly' }
// Yearly on the same date. 29 February runs in leap years only.
{ freq: 'yearly' }Frequencies
Four frequencies, each with an interval. A weekly rule takes the weekdays it falls on;
without them it uses the weekday of start. A
monthly rule repeats on the day of the month start falls on and skips the months that lack it, rather than sliding to the last day. A yearly rule
on 29 February runs in leap years only.
// Every day, or every other day
{ freq: 'daily' }
{ freq: 'daily', interval: 2 }
// Weekly on the weekday of start; or on the days listed (0 is Sunday).
// A week is counted from weekStart, else the scheduler's weekStartsOn.
{ freq: 'weekly' }
{ freq: 'weekly', byDay: [1, 3, 5] }
{ freq: 'weekly', interval: 2, byDay: [5], weekStart: 1 } // every other Friday
// Monthly on the day of the month start falls on. A month without that day is
// skipped: a series from the 31st runs in January, March, May...
{ freq: 'monthly' }
// Yearly on the same date. 29 February runs in leap years only.
{ freq: 'yearly' }Interval, Count and Until
count includes the first occurrence. until is the latest start an occurrence may have and is inclusive. When both are set, count wins.
// count includes the first occurrence
{ freq: 'weekly', byDay: [2, 4], count: 6 }
// until is the latest start allowed, inclusive; any date form the scheduler reads
{ freq: 'weekly', byDay: [3], until: '2026-12-31T23:59' }
// With both, count wins
{ freq: 'daily', count: 10, until: '2027-01-01' } // ten daysSkipping Dates
exDates removes single occurrences. An entry has to match the start of an occurrence exactly, minute for
minute; a date-only string names midnight and matches nothing that starts at noon.
// Skip single occurrences. Each entry must match an occurrence's start exactly.
{
freq: 'weekly',
byDay: [4],
exDates: ['2026-09-24T12:00', '2026-10-01T12:00'] // no lunch those two Thursdays
}
// Strings here are read in the zone of the series: the scheduler's timeZone,
// or the zone of start when start is a ZonedDateTime.Occurrences Are Read Only
An occurrence cannot be dragged, resized or deleted, and it has no delete button in its
details. It carries a seriesId pointing at the event that holds the rule, and an id of the form seriesId@instant. To change one date, change the rule: add the date to exDates to skip it, or set until to end the series. Click an occurrence below; its panel offers both.
Oct 19 – 25, 2026
<script lang="ts">
import { Scheduler, type EventInput, type SchedulerEvent } from '@sv5ui/scheduler';
let events = $state<EventInput[]>([/* ... */]);
// An occurrence points back at its series through seriesId. Editing means
// changing the rule on the series in the array; the occurrences follow.
function skip(occurrence: SchedulerEvent) {
const series = events.find((event) => event.id === occurrence.seriesId);
if (!series?.recurrence) return;
series.recurrence = {
...series.recurrence,
exDates: [...(series.recurrence.exDates ?? []), occurrence.start]
};
}
function endBefore(occurrence: SchedulerEvent) {
const series = events.find((event) => event.id === occurrence.seriesId);
if (!series?.recurrence) return;
series.recurrence = { ...series.recurrence, until: occurrence.start.subtract({ minutes: 1 }) };
}
</script>
<Scheduler bind:events detail="slideover" eventPanel={panel} />
{#snippet panel({ event, close })}
{#if event.seriesId}
<Button label="Skip this date" onclick={() => { skip(event); close(); }} />
<Button label="End the series before this" onclick={() => { endBefore(event); close(); }} />
{/if}
{/snippet}Series in Their Own Time Zone
A series whose start is a ZonedDateTime repeats in that zone: the weekday test, the time of day and any string in until or exDates are read there, and each occurrence is then shown in the calendar's zone. A series written as
strings belongs to the calendar's zone and moves with it. Below, a Sydney standup and a local
one, both "9:00 every weekday", seen from Ho Chi Minh City. See Time Zones.
Oct 19 – 25, 2026
import { parseZonedDateTime } from '@internationalized/date';
// A series given as a ZonedDateTime repeats in its own zone. The Sydney
// standup is at 9:00 Sydney every weekday, whatever zone the calendar shows;
// in Ho Chi Minh City that is 6:00 in winter and 5:00 in Sydney's summer, and a
// Monday standup lands on Monday in Sydney's week, not the calendar's.
{
id: 'sydney-standup',
title: 'Sydney standup',
start: parseZonedDateTime('2026-03-02T09:00[Australia/Sydney]'),
end: parseZonedDateTime('2026-03-02T09:30[Australia/Sydney]'),
recurrence: { freq: 'weekly', byDay: [1, 2, 3, 4, 5] }
}
// A series given as strings repeats in the calendar's timeZone, and moves with it.Daylight Saving
An occurrence keeps its clock time and its length across the days the clocks change: a 9:00 to 10:00 standup stays 9:00 to 10:00 the Monday after, in its own zone. Ho Chi Minh City has no daylight saving; the Sydney series above shifts by an hour against it twice a year, which is the correct answer.
Supported Fields
The type describes the full RFC 5545 grammar so data from another system keeps its shape.
The engine evaluates the subset below; a field it does not evaluate is ignored with one
warning per series in development and never throws. There is no way to edit a single
occurrence into something different from its series, and no rDates.
RecurrenceRule
| Field | Evaluated |
|---|---|
freq | Yes |
interval | Yes |
byDay | Plain weekdays |
count | Yes |
until | Yes |
exDates | Yes |
weekStart | Yes |
rDates | No |
bySetPos | No |
byMonthDay | No |
byMonth | No |
byWeekNo | No |
byYearDay | No |
// Accepted on the type so stored data keeps its shape, not evaluated. Each
// series with one of these prints one warning in development and is otherwise
// expanded as if the field were absent.
{
freq: 'monthly',
byDay: [{ day: 1, ordinal: -1 }], // "last Monday": the ordinal is dropped; a weekly rule would read plain Monday
byMonthDay: [15],
bySetPos: [1],
byMonth: [1, 7],
byWeekNo: [1],
byYearDay: [100],
rDates: ['2026-12-24T09:00']
}
// [@sv5ui/scheduler] Event "x" uses recurrence fields the built-in engine does not evaluate: rDates, bySetPos, byMonthDay, byMonth, byWeekNo, byYearDay, byDay ordinals.Limits
A series is expanded to at most a thousand occurrences per visible range, which no view reaches: a daily rule across the six weeks of a month grid is forty two.
Props
The props this page uses.
| Prop | Type | Default |
|---|---|---|
events | EventInput<T>[] | - |
weekStartsOn | WeekDay | 1 |
timeZone | TimeZoneId | local zone |
eventPanel | Snippet<[EventPanelSnippetProps<T>]> | - |
Snippets
What the snippets on this page receive. Every type is exported from @sv5ui/scheduler.
| Type | Fields |
|---|---|
EventPanelSnippetProps | event, close, remove, update, deletable |