---
title: Announcing the Capacitor Calendar Plugin
description: Our new Capacitor calendar plugin reads recurrence rules back off an event and reports calendar changes made by other apps through a listener.
date:
  created: 2026-09-09
  updated: 2026-09-09
authors:
  - robingenz
categories:
  - Announcements
  - Capacitor
  - SDKs
links:
  - Capacitor Calendar: sdks/capacitor/calendar.md
faq: true
---

# Announcing the Capacitor Calendar Plugin

Today we're releasing the [Capacitor Calendar plugin](../../sdks/capacitor/calendar.md), and two capabilities drove its design. Recurrence rules round-trip: you write a rule when you create an event, and you read the same rule back off that event later, instead of writing a rule your app can never see again. And a `calendarChange` listener tells your app when calendars or events change, including edits the user made in the calendar app or in another app entirely. Around those sit the ordinary parts of calendar work, from calendars and alerts to single-occurrence edits and the system event dialogs. It's available today to all Capawesome [Insiders](../../insiders/index.md).

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

**Key takeaways:**

- `@capawesome-team/capacitor-calendar` covers Android and iOS with one API for calendars, events, recurrence, alerts, and the system event dialogs, and requires Capacitor 8 or later.
- The `recurrence` property is both written and read, so a recurring event can pass through your app without losing its rule.
- [`getEvents(...)`](../../sdks/capacitor/calendar.md#getevents) expands a series into occurrences; pass `instanceStartDate` and `span` to [`updateEventById(...)`](../../sdks/capacitor/calendar.md#updateeventbyid) or [`deleteEventById(...)`](../../sdks/capacitor/calendar.md#deleteeventbyid) to change one occurrence, or that occurrence and every later one.
- The `calendarChange` event fires when calendars or events are created, updated, or deleted, including by other apps.
- Permissions are split into `readCalendar` and `writeCalendar`. On iOS 17 and newer, requesting only `writeCalendar` requests write-only access, which is not enough for the methods of this plugin.
- All-day events are midnight UTC on both platforms, with an exclusive `endDate`, so a one-day event never lands on the wrong day.

## Which Capacitor calendar plugin supports recurring events?

Every maintained Capacitor calendar plugin can create a recurring event. The differences start after the write: whether your app can read the rule back, and whether it can change a single occurrence without rewriting the series.

The table below compares the Capacitor Calendar plugin with the two other options in the ecosystem, checked on 2026-09-07 against `@ebarooni/capacitor-calendar` 8.6.0 and `@capgo/capacitor-calendar` 8.0.8, which is a [fork](https://github.com/Cap-go/capacitor-calendar){:target="_blank"} of the former. "Not documented" means the README of that plugin does not describe the capability, not that it is impossible.

| Capability | Capacitor Calendar plugin | @ebarooni 8.6.0 | @capgo 8.0.8 |
| --- | --- | --- | --- |
| Create recurring events | Yes | Yes | Yes |
| Rule parts | `frequency`, `interval`, `count`, `until`, `daysOfWeek` | the same plus `byMonth`, `byMonthDay`, `byWeekDay`, and `daysOfTheYear` / `weeksOfTheYear` on iOS | same as @ebarooni |
| Read the rule back off an event | Yes, via `recurrence` on the returned event | Not documented | Not documented |
| Update a single occurrence | Yes, on Android and iOS | `span` documented for iOS only | `span` only, no occurrence date |
| Delete a single occurrence | Yes, on Android and iOS | Yes, `instanceDate` documented for Android | `span` only, no occurrence date |
| Listener for changes made by other apps | `calendarChange` | Not documented | Not documented |
| Write-only calendar access on iOS 17+ | Yes | Yes | Yes |
| iOS Reminders API | No | Yes | Yes |
| Web implementation | No | Partial, exports an `.ics` file | No |
| License | Capawesome Insiders (paid) | MIT | MPL-2.0 |

The read-back row decides whether your app can show a user "repeats every second Monday" for an event it did not create in this session. The occurrence rows decide whether a user can skip one training session, or move a weekly class from a given week onwards, without losing the rest of the series.

The Reminders row points the other way. If your app needs the iOS Reminders app rather than the Calendar app, the other two plugins cover it and this one does not.

## Installation

To install the Capacitor Calendar plugin, please refer to the [Installation](../../sdks/capacitor/calendar.md/#installation) section in the plugin documentation. It ships through the Capawesome npm registry, so installing it needs the license key that comes with a [Capawesome Insiders](../../insiders/index.md) subscription.

## Usage

The tour below follows one event through its whole life: permission, calendar, creation, reading, editing, and deletion.

### Request calendar permissions

[`requestPermissions(...)`](../../sdks/capacitor/calendar.md#requestpermissions) asks for read and write access by default, and the `permissions` option narrows the request to a subset:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const requestPermissions = async () => {
  const { readCalendar, writeCalendar } = await Calendar.requestPermissions();
  return readCalendar === 'granted' && writeCalendar === 'granted';
};
```

On iOS 17 and newer, the subset you request changes which access level the system grants. Requesting only `writeCalendar` maps to EventKit's write-only access, which lets an app add events without seeing the events of the user. It is also not sufficient for the methods of this plugin, because creating an event means looking up its calendar first. Two consequences follow. `readCalendar` stays `prompt` for as long as only write-only access has been granted, so treat `prompt` as a real state rather than as "not asked yet". And the `Info.plist` key you need depends on what you request: `NSCalendarsFullAccessUsageDescription` for the full-access path on iOS 17 and newer, [`NSCalendarsWriteOnlyAccessUsageDescription`](https://developer.apple.com/documentation/bundleresources/information-property-list/nscalendarswriteonlyaccessusagedescription){:target="_blank"} when you request write-only, and `NSCalendarsUsageDescription` for iOS 16 and older, which draws no line between reading and writing. A missing key makes `requestPermissions(...)` reject with an explicit error instead of failing silently.

Android splits the same access into `READ_CALENDAR` and `WRITE_CALENDAR` in the manifest, with one pairing rule that catches people out: `createEvent(...)`, `updateEventById(...)` and `deleteEventById(...)` need `READ_CALENDAR` on top of `WRITE_CALENDAR`, because they resolve the calendar or the event before they touch it. Only [`createCalendar(...)`](../../sdks/capacitor/calendar.md#createcalendar) and [`deleteCalendarById(...)`](../../sdks/capacitor/calendar.md#deletecalendarbyid) get by with the write permission alone.

### Pick a calendar

A device usually carries several calendars, and a subscribed holiday feed will reject anything you write to it. [`getCalendars()`](../../sdks/capacitor/calendar.md#getcalendars) returns all of them with a `writable` flag, an `id`, a `title`, and a `#RRGGBB` color, so filtering is a one-liner:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const getWritableCalendars = async () => {
  const { calendars } = await Calendar.getCalendars();
  return calendars.filter(calendar => calendar.writable);
};
```

When you don't want to show a picker at all, [`getDefaultCalendar()`](../../sdks/capacitor/calendar.md#getdefaultcalendar) returns the calendar the system uses for new events, or `null` when the device has none. You can also create your own calendar with `createCalendar(...)` and give it a title and a color, which keeps the events your app writes visually separate from everything else in the user's week.

### Create an event with alerts

[`createEvent(...)`](../../sdks/capacitor/calendar.md#createevent) needs a `title` and a `startDate`, and falls back to the default calendar and a one-hour duration for everything you leave out. The `alerts` array holds offsets in minutes before the start, and negative numbers move an alert after the start:

```typescript
import { Calendar, EventAvailability } from '@capawesome-team/capacitor-calendar';

const createEvent = async (calendarId: string) => {
  const startDate = new Date('2026-09-15T10:00:00').getTime();
  const { id } = await Calendar.createEvent({
    event: {
      calendarId,
      title: 'Dentist appointment',
      startDate,
      endDate: startDate + 60 * 60 * 1000,
      location: 'Main Street 1, Springfield',
      description: 'Bring the insurance card.',
      availability: EventAvailability.Busy,
      alerts: [60, 15],
    },
  });
  return id;
};
```

The operating system fires those two alerts whether or not your app is running, so a confirmed booking reminds the user 60 and 15 minutes ahead without a push notification, a server, or a background task.

### Create a recurring event

Adding a `recurrence` rule turns the same call into a series. The rule takes a `frequency`, an `interval`, an end condition, and an optional set of weekdays:

```typescript
import {
  Calendar,
  RecurrenceFrequency,
  Weekday,
} from '@capawesome-team/capacitor-calendar';

const createRecurringEvent = async () => {
  const { id } = await Calendar.createEvent({
    event: {
      title: 'Team stand-up',
      startDate: new Date('2026-09-15T09:00:00').getTime(),
      recurrence: {
        frequency: RecurrenceFrequency.Weekly,
        interval: 1,
        count: 10,
        daysOfWeek: [Weekday.Monday, Weekday.Wednesday],
      },
    },
  });
  return id;
};
```

`count` ends the series after a number of occurrences and takes precedence over `until`, which ends it on a date. `frequency` accepts `Daily`, `Weekly`, `Monthly`, and `Yearly`, and `interval` multiplies it, so an interval of 2 with a weekly frequency gives you a fortnightly event.

Every event you read back carries its `recurrence`, so a course app can render "every second Tuesday until December" for a series it did not create itself. A rule written by another app that uses parts the plugin does not model is read back as the closest supported subset, with `frequency` and `interval` always correct.

### Read the events in a range

[`getEvents(...)`](../../sdks/capacitor/calendar.md#getevents) returns every event that overlaps a time range, across all calendars unless you pass a `calendarId`:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const getEventsOfNextWeek = async () => {
  const from = Date.now();
  const to = from + 7 * 24 * 60 * 60 * 1000;
  const { events } = await Calendar.getEvents({ from, to });
  return events;
};
```

Recurring events arrive expanded. Each occurrence is a separate entry with its own `startDate` and `endDate`, while the `id` and the `recurrence` rule are shared across the whole series. That shape is what an agenda view wants, and it is also why you cannot address an occurrence by `id` alone.

[`getEventById(...)`](../../sdks/capacitor/calendar.md#geteventbyid) takes the other view: it returns the series itself with its original start date, and resolves with `null` when the event is gone. Passing a `calendarId` that no longer exists to `getEvents(...)` rejects with the error code `CALENDAR_NOT_FOUND`, so a stale identifier in your own database is something you can branch on rather than something you have to parse out of a message.

### Update an event, and clear fields

`updateEventById(...)` applies a patch. Properties you leave out keep their current values, and properties you set to `null`, or to `[]` for an array, are removed from the event:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const rescheduleEvent = async (id: string, startDate: number) => {
  await Calendar.updateEventById({
    id,
    event: { startDate, endDate: startDate + 30 * 60 * 1000 },
  });
};

const clearEventDetails = async (id: string) => {
  await Calendar.updateEventById({
    id,
    event: { location: null, description: null, alerts: [] },
  });
};
```

Five properties hold the structure of an event together and must never be `null`: `allDay`, `calendarId`, `endDate`, `startDate`, and `timezone`. Everything else is fair game, which makes "the customer removed the meeting room from the booking" a single call.

### Change one occurrence, or every later one

By default, `updateEventById(...)` and `deleteEventById(...)` apply to the whole series. Pass the `startDate` of an occurrence as `instanceStartDate`, and the operation targets that occurrence instead. The `span` option then decides how far the change reaches:

```typescript
import { Calendar, EventSpan } from '@capawesome-team/capacitor-calendar';

const skipOneSession = async (id: string, instanceStartDate: number) => {
  await Calendar.deleteEventById({
    id,
    instanceStartDate,
    span: EventSpan.ThisEvent,
  });
};

const endSeriesFromHere = async (id: string, instanceStartDate: number) => {
  await Calendar.deleteEventById({
    id,
    instanceStartDate,
    span: EventSpan.ThisAndFutureEvents,
  });
};
```

`EventSpan.ThisEvent` is the default and leaves the rest of the series untouched. `EventSpan.ThisAndFutureEvents` also rewrites every later occurrence while past ones stay as they were, which is the behavior a user expects when a weekly class moves to a new time in the middle of a term. Omit `instanceStartDate` and you are back to editing or deleting the entire series.

### Hand the event to the system dialog

For flows where the user should confirm the event themselves, [`displayCreateEvent(...)`](../../sdks/capacitor/calendar.md#displaycreateevent) opens the system event editor, prefilled with whatever you pass:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const displayCreateEvent = async () => {
  const { id } = await Calendar.displayCreateEvent({
    event: {
      title: 'Lunch with Jane',
      startDate: new Date('2026-09-15T12:00:00').getTime(),
      location: 'Main Street 1, Springfield',
    },
  });
  return id;
};
```

The user sees a screen they already know, picks the calendar, and confirms. [`displayUpdateEventById(...)`](../../sdks/capacitor/calendar.md#displayupdateeventbyid) does the same for an existing event and reports back an `action` of `saved`, `canceled`, or `deleted`. Both results are iOS-only, because the Android system intent does not report anything to the calling app, and Android also prefills only the properties its intent supports and drops the rest.

### React to changes made outside your app

The `calendarChange` event fires whenever calendars or events are created, updated, or deleted on the device, including by the calendar app and by other apps:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const addCalendarChangeListener = async () => {
  return Calendar.addListener('calendarChange', () => {
    console.log('The calendars or events on the device have changed.');
  });
};
```

The event carries no payload, because neither operating system reports which entity changed. Treat it as a signal to re-run your `getEvents(...)` query for the range you are showing. An agenda screen then updates after the user drags an appointment to another day in the calendar app, instead of refetching on every app resume and hoping the timing works out.

Two smaller helpers complete the API. [`openCalendar(...)`](../../sdks/capacitor/calendar.md#opencalendar) opens the calendar app of the device at a given date, which pairs well with a "view in calendar" button after you created an event, and [`openSettings()`](../../sdks/capacitor/calendar.md#opensettings) sends the user to your app settings so a denied permission can still be granted.

## All-Day Events and Time Zones

All-day events follow one documented contract on both platforms. An event with `allDay: true` has no time of day, and its `startDate` and `endDate` are interpreted and returned as midnight UTC of the respective calendar day on Android and iOS alike, independent of the time zone of the device.

The `endDate` is exclusive. A one-day all-day event on 2026-09-01 has `startDate = Date.UTC(2026, 8, 1)` and `endDate = Date.UTC(2026, 8, 2)`. An `endDate` that is missing or not on a midnight boundary is normalized: floored to midnight UTC of its day, then raised to at least one day after `startDate`, so the event always spans a full day. Build these timestamps with `Date.UTC(...)` and format them in UTC as well. Using the local time zone for either half is what shifts a birthday to the day before or after for every user whose offset is not zero.

Events that are not all-day work the other way around. The `timezone` property holds an IANA identifier such as `Europe/Berlin` and defaults to the time zone of the device, which matters for a booking that must stay at 10:00 local time even after the user flies somewhere else.

## Platform Differences Worth Knowing

The plugin documents the places where Android and iOS disagree instead of faking parity:

- **Dialog results are iOS-only.** The `id` from `displayCreateEvent(...)` and the `action` from `displayUpdateEventById(...)` come back on iOS. Android's system intent reports nothing to the app.
- **`EventAvailability.Unavailable` becomes `Busy` on Android.** The plugin exposes four availability values, and Android maps that one down.
- **Android needs the read permission to write.** Creating, updating, and deleting an event requires `READ_CALENDAR` alongside `WRITE_CALENDAR`.
- **iOS write-only access does not unlock the API.** It lets an app add events without seeing the calendar of the user, but the methods of this plugin need full access.
- **Android and iOS only.** There is no web implementation, so guard calendar features behind a platform check in a browser build.

## FAQ

### Do I need calendar read permission to create an event?

On Android, yes: `createEvent(...)` requires `READ_CALENDAR` in addition to `WRITE_CALENDAR`, because it resolves the calendar before writing. On iOS 17 and newer, the methods of the plugin need full calendar access as well. Write-only access can be requested with `requestPermissions({ permissions: ['writeCalendar'] })`, but it is not sufficient for these methods.

### Can I delete a single occurrence of a recurring event?

Yes. Pass the `startDate` of the occurrence, as returned by `getEvents(...)`, to `deleteEventById(...)` as `instanceStartDate`. `EventSpan.ThisEvent` removes only that occurrence, and `EventSpan.ThisAndFutureEvents` removes it together with every later one. The same two options apply to `updateEventById(...)`.

### Why does my all-day event show up on the wrong day?

Because the timestamps were built in the local time zone. All-day events use midnight UTC on both platforms, and the `endDate` is exclusive, so use `Date.UTC(...)` to build them and format them in UTC when you display them.

### Does the Capacitor Calendar plugin support iOS Reminders?

No. It covers calendars and events on Android and iOS. If your app needs the iOS Reminders app, `@ebarooni/capacitor-calendar` and `@capgo/capacitor-calendar` both offer a Reminders API.

### Is the Capacitor Calendar plugin free?

No. It is part of the [Capawesome Insiders](../../insiders/index.md) subscription, which also covers every other Insiders plugin and priority support from the Capawesome team.

### Does it work with Ionic, Angular, React, or Vue?

Yes. The plugin is framework-agnostic and works in any Capacitor app, including Ionic with Angular, React, or Vue, as well as plain JavaScript projects.

## Availability

The Capacitor Calendar plugin is available today to all Capawesome [Insiders](../../insiders/index.md) and requires Capacitor 8 or later. It supports both CocoaPods and Swift Package Manager on iOS, and pairs with the [Capacitor Contacts plugin](../../sdks/capacitor/contacts.md) for inviting people to an event and the [Capacitor Datetime Picker plugin](../../sdks/capacitor/datetime-picker.md) for picking a start date natively.

New plugins and notable releases are announced in the Capawesome newsletter first.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter/){ .md-button .md-button--primary }

## Conclusion

If you are choosing between the three Capacitor calendar plugins, decide on the two rows in the table that your feature depends on. An app that writes appointments and forgets about them is well served by any of them. An app that shows the user their own recurring events, edits one occurrence of a series, or has to stay in sync with edits made in the calendar app will run into the read-back and listener gaps quickly, and that is the case the [Capacitor Calendar plugin](../../sdks/capacitor/calendar.md) was built for.

**Further reading:**

- [Announcing the Capacitor Contacts Plugin](./announcing-the-capacitor-contacts-plugin.md) — read and write device contacts to invite them to an event
- [API Reference](../../sdks/capacitor/calendar.md#api) — every method, option, and enum

**Missing a feature?** [Create a feature request](https://github.com/capawesome-team/capacitor-plugins/issues/new/choose){:target="_blank"} in our GitHub repository.

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
