Recurrence Scope

Reduces edits that hit more of a recurring series than intended by listing exactly which dates each scope choice changes.

StopsEdited whole series instead of one

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/recurrence-scope.json

The files land in your project. They're yours to edit.

Usage

recurrence-scope-usage.tsx
import { RecurrenceScope, type Occurrence } from "@/components/ui/recurrence-scope"

<RecurrenceScope
  title="Team stand-up"
  occurrences={occurrences}
  editingId="2026-10-20"
  changes={["time"]}
  fieldLabels={{ time: "start time", title: "title" }}
  continues
  today="2026-10-13"
  defaultValue="following"
  onCancel={() => {}}
  onConfirm={() => new Promise((resolve) => setTimeout(resolve, 600))}
/>

What it catches

  • Exactly which occurrences each scope touches, with a count
  • Past occurrences included in the change
  • Occurrences with their own edits that would be overwritten, by date and field
  • A repeat-rule change, which regenerates (and overwrites) the whole affected range
  • Option to keep individually edited occurrences

Props

RecurrenceScope props
PropTypeDefault
title* (required)

Name of the event or task, e.g. "Team stand-up".

string—
occurrences* (required)

The series' occurrences in order (a finite window is fine; see `continues`).

Occurrence[]—
editingId* (required)

The occurrence the user opened.

string—
changes* (required)

Fields being changed, e.g. `["time"]`. Include `"repeat"` when the rule itself changes.

string[]—
fieldLabels

Words for fields in the warning, e.g. `{ time: "time", title: "title" }`.

Record<string, string>{}
continues

The series carries on after the last listed occurrence (no end date).

booleanfalse
today

"Today" as `YYYY-MM-DD`, to call out past occurrences. Defaults to the local date once mounted.

string—
value

Controlled scope.

RecurrenceScopeValue—
defaultValue

Initial scope when uncontrolled. Defaults to the safest, "this".

RecurrenceScopeValue"this"
onValueChange

Called when the scope changes.

(scope: RecurrenceScopeValue) => void—
onConfirm

Save. `skip` lists occurrences whose own edits should be kept. May return a Promise.

(scope: RecurrenceScopeValue, detail: { ids: string[]; skip: string[] }) => void | Promise<unknown>—
onCancel

Close without saving.

() => void—
locale

Locale for dates.

string"en-IN"
scopeLabels

Option labels.

Record<RecurrenceScopeValue, string>{ this: "This one", following: "This and following", all:…
overwriteText

Templates: `{dates}` and `{fields}` are filled in.

string"{dates} {verb} edited on their own. Saving overwrites th…
keepLabel

Label of the button that keeps individually edited occurrences.

string"Keep their own changes"
cancelLabel

Label of the Cancel button.

string"Cancel"
saveLabel

Save button: `{count}` is filled in.

string"Save {count}"
messages

Override the readback and other small strings (see `RECURRENCE_MESSAGES`).

Partial<RecurrenceMessages>—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Scope options are a radiogroup labelled by the title, built from labelled radio inputs
  • Changed dates list has an aria-label; changed/kept/unchanged state shown as text, not colour alone
  • Count and overwrite warning render in polite live regions
  • Save disables while pending, with an aria-hidden spinner

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Works on the occurrences you pass in; it does not expand RRULEs itself
  • Does not save anything; onConfirm receives the choice

What it stops

Edited whole series instead of one

Also known as

recurring event · repeat · rrule · calendar · this event only · all events · google calendar · series

Field type

Dependencies

Pairs well with