Events & Data

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

Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1

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 days

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

3 series in the array

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
<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

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
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

FieldEvaluated
freqYes
intervalYes
byDayPlain weekdays
countYes
untilYes
exDatesYes
weekStartYes
rDatesNo
bySetPosNo
byMonthDayNo
byMonthNo
byWeekNoNo
byYearDayNo
// 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.

PropTypeDefault
eventsEventInput<T>[]-
weekStartsOnWeekDay1
timeZoneTimeZoneIdlocal zone
eventPanelSnippet<[EventPanelSnippetProps<T>]>-

Snippets

What the snippets on this page receive. Every type is exported from @sv5ui/scheduler.

TypeFields
EventPanelSnippetPropsevent, close, remove, update, deletable