---
title: "Javascript Event calendar Google Calendar Sync Example | Mobiscroll"
component: "Event calendar"
framework: "JavaScript"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/javascript/eventcalendar/sync-events-google-calendar"
---

# Google Calendar Sync - JavaScript Event calendar demo

Connect Google Calendar to a Javascript event calendar — authenticate users, load events, and sync create, edit, and delete actions to Google Calendar. Plain JavaScript API.

- [Live demo](https://demo.mobiscroll.com/javascript/eventcalendar/sync-events-google-calendar)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-javascript/tree/main/src/demos/eventcalendar/calendar-view/sync-events-google-calendar)
- This demo in other frameworks: [jQuery](https://demo.mobiscroll.com/jquery/eventcalendar/sync-events-google-calendar.md), [Angular](https://demo.mobiscroll.com/angular/eventcalendar/sync-events-google-calendar.md), [React](https://demo.mobiscroll.com/react/eventcalendar/sync-events-google-calendar.md), [Vue](https://demo.mobiscroll.com/vue/eventcalendar/sync-events-google-calendar.md)

## Description

The event calendar comes with third party calendar integration support through the [**integration plugin**](https://mobiscroll.com/docs/javascript/eventcalendar/calendar-integrations). This needs to be added to the download package or installed separately from a dedicated NPM package.

It includes everything you need to authenticate your users, get their Google calendars and provides functions for CRUD operations. It takes care of loading the events and converts them to the required format so that they can be displayed on the Mobiscroll calendar separately or mixed with other calendars and events.

Customizing the interaction, event workflows, whether the events are read-only is up to you. In this live demo, newly created events will land in the `primary calendar` of the authenticated user.

This example uses the [**integration plugin**](https://mobiscroll.com/docs/javascript/eventcalendar/calendar-integrations) to connect to and load events from Google Calendar and it is not available in the trial. Give the live demo a try and see how it can be configured.

## What this demo shows

- Shows how Mobiscroll event calendar can load and display events from a signed-in Google Calendar account in a desktop month view.
- **Layout** The demo uses a full month calendar layout with a separate panel on the left for Google authentication, editing settings, and calendar selection.
- **Signed-out state** No events are loaded by default, and the side panel prompts the user to sign in with the text "Log into your Google account to view and edit your Google Calendar events" followed by a `Sign in with Google` button.
- **Authentication flow** Clicking the Google sign-in button opens a new browser tab where the user can authenticate with their Google account.
- **Signed-in state** After sign-in, the side panel changes to an authenticated state with guidance about editing events and syncing changes back to Google Calendar.
- **Editing toggle** The authenticated panel includes an `Enable editing` switch, which is off by default and controls whether event edits are allowed or not.
- **Editing toggle enabled** Editing an event opens a popup with the following message `Are you sure you want to update this event? This actin will affect your Google Calendar event.` and below that ther is a `Cancel` and `Update` button, by clicking the firs one, the popup closes and the event editing won't happen and by clicking the second button will update the event on Mobiscroll calendar and sync back to the Google Calendar as well. 
- **Event creation or update** If a new event is created or an existing one updated, a `Event created/updated/ deleted in [the name of your Google Calendar] calendar` toast appears at the bottom center of the calendar.
- **Calendar list** The `My Calendars` section shows multiple calendar switches, such as the user's personal calendar and country-specific holiday calendars.
- **Sign-out** The bottom of the side panel includes a `Log out of my account` button for disconnecting the Google Calendar account.
- **Event rendering** Once events are loaded, days with events display event labels inside the month cells.
- **Event styling** Event labels can appear with different visual styles based on the type or source of the event they represent.
- **Event interaction** Hovering over or selecting an event label highlights it. If event editing is enabled this action shows resize handles on both sides, indicating that the event can be resized by clicking and dragging.
- **Day cell states** Hovering a day cell highlights the day number with a gray background, while clicking the empty part of the cell selects the day and highlights the day number with a blue background.
- **Header navigation** The calendar header shows the current month and year on the left, and blue month navigation arrows with a `Today` button between them on the right.
- **Swipe navigation** When event editing is disabled, the displayed month can also be changed by dragging the calendar left or right.

## Best for

- **Personal scheduling interfaces** Building calendar views where users need to browse and manage their own events from an existing Google Calendar.
- **Calendar overlay experiences** Letting users toggle multiple Google calendars, such as personal and holiday calendars, within the same event calendar.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-themeVariant) - option
- [`setEvents`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-setEvents) - method
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`labels`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-labels) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-extendDefaultEvent) - option
- [`exclusiveEndDates`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-exclusiveEndDates) - option
- [`clickToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToResize) - option
- [`renderHeader`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderHeader) - renderer
- [`onPageLoading`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreate) - event
- [`removeEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-removeEvent) - method
- [`addEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-addEvent) - method
- [`onEventUpdate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdate) - event
- [`updateEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-updateEvent) - method
- [`onEventDelete`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDelete) - event
- [`setOptions`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-setOptions) - method

## Code

### JS

```js
mobiscroll.setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

var googleCalendarSync = mobiscroll.googleCalendarSync;

var loginButton = document.getElementById('demo-google-cal-sign-in');
var logoutButton = document.getElementById('demo-google-cal-sign-out');

var calendarList = document.getElementById('demo-google-cal-list');
var loggedOutCont = document.getElementById('demo-logged-out-cont');
var loggedInCont = document.getElementById('demo-logged-in-cont');
var cont = document.getElementById('demo-google-calendar-cont');
var editButton = document.getElementById('demo-google-cal-edit');

var startDate;
var endDate;
var calendarIds = [];
var events = [];
var calendarData = {};
var primaryCalendarId;
var debounce;

function toggleContainers(loggedIn) {
  if (loggedIn) {
    loggedOutCont.style.display = 'none';
    loggedOutCont.ariaHidden = true;
    loggedInCont.style.display = 'block';
    loggedInCont.ariaHidden = false;
  } else {
    loggedInCont.style.display = 'none';
    loggedInCont.ariaHidden = true;
    loggedOutCont.style.display = 'block';
    loggedOutCont.ariaHidden = false;
  }
}

function loadEvents(checked, calendarId) {
  if (checked) {
    calendarIds.push(calendarId);
  } else {
    var index = calendarIds.indexOf(calendarId);
    if (index !== -1) {
      calendarIds.splice(index, 1);
    }
  }
  if (calendarIds.length === 0) {
    events = [];
    inst.setEvents(events);
    return;
  }
  loadingEvents(true);
  googleCalendarSync
    .getEvents(calendarIds, startDate, endDate)
    .then(function (resp) {
      loadingEvents(false);
      events = resp;
      inst.setEvents(events);
    })
    .catch(onError);
}

function onSignedIn() {
  toggleContainers(true);
  googleCalendarSync
    .getCalendars()
    .then(function (calendars) {
      var calList = '<div class="mbsc-form-group-title">My Calendars</div>';

      calendars.sort(function (c) {
        return c.primary ? -1 : 1;
      });
      primaryCalendarId = calendars[0].id;
      calendarIds.push(primaryCalendarId);

      for (var i = 0; i < calendars.length; ++i) {
        var c = calendars[i];
        calList +=
          '<label>' +
          '<input mbsc-switch id="' +
          i +
          '" value="' +
          c.id +
          '" data-label="' +
          c.summary +
          '" type="checkbox" class="google-calendar-switch"' +
          (i == 0 ? 'checked' : '') +
          ' />' +
          '</label>';

        calendarData[c.id] = { name: c.summary, color: c.backgroundColor };
      }

      calendarList.innerHTML = calList;
      mobiscroll.enhance(calendarList);

      loadingEvents(true);

      googleCalendarSync
        .getEvents([primaryCalendarId], startDate, endDate)
        .then(function (resp) {
          loadingEvents(false);
          events = resp;
          inst.setEvents(events);
        })
        .catch(onError);
    })
    .catch(onError);
}

function onSignedOut() {
  toggleContainers(false);
  calendarIds = [];
  calendarData = [];
  events = [];
  inst.setEvents([]);
  calendarList.innerHTML = '';
}

function onError(resp) {
  mobiscroll.toast({
    message: resp.error ? resp.error : resp.result.error.message,
  });
}

function loadingEvents(show) {
  if (show) {
    cont.classList.add('md-loading-events');
  } else {
    cont.classList.remove('md-loading-events');
  }
}

var inst = mobiscroll.eventcalendar('#demo-google-calendar', {
  view: {
    calendar: { labels: true },
  },
  extendDefaultEvent: function () {
    if (primaryCalendarId) {
      return {
        color: calendarData[primaryCalendarId].color,
      };
    }
  },
  exclusiveEndDates: true,
  clickToCreate: false,
  dragToCreate: false,
  dragToMove: false,
  dragToResize: false,
  renderHeader: function () {
    return (
      '<div mbsc-calendar-nav></div>' +
      '<div class="md-spinner">' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '</div>' +
      '<div class="md-google-calendar-header">' +
      '<button mbsc-calendar-prev></button>' +
      '<button mbsc-calendar-today>Today</button>' +
      '<button mbsc-calendar-next></button></div>'
    );
  },
  onPageLoading: function (args) {
    startDate = args.viewStart;
    endDate = args.viewEnd;
    clearTimeout(debounce);
    debounce = setTimeout(function () {
      if (googleCalendarSync.isSignedIn()) {
        loadingEvents(true);
        googleCalendarSync
          .getEvents(calendarIds, startDate, endDate)
          .then(function (resp) {
            loadingEvents(false);
            events = resp;
            inst.setEvents(events);
          })
          .catch(onError);
      }
    }, 200);
  },
  onEventCreate: function (args) {
    if (googleCalendarSync.isSignedIn()) {
      var event = args.event;
      googleCalendarSync
        .addEvent(primaryCalendarId, event)
        .then(function (resp) {
          inst.removeEvent(event);
          resp.color = event.color;
          inst.addEvent(resp);
          events.push(resp);
          mobiscroll.toast({
            message: 'Event created in "' + calendarData[primaryCalendarId].name + '" calendar',
          });
        })
        .catch(function (error) {
          onError(error);
          inst.removeEvent(args.event);
        });
    }
  },
  onEventUpdate: function (args) {
    if (googleCalendarSync.isSignedIn()) {
      mobiscroll
        .confirm({
          title: 'Are you sure you want to update this event?',
          message: 'This action will affect your Google Calendar event.',
          okText: 'Update',
        })
        .then(function (result) {
          if (result) {
            var event = args.event;
            var calendarId = event.googleCalendarId;
            googleCalendarSync
              .updateEvent(calendarId, event)
              .then(function () {
                mobiscroll.toast({
                  message: 'Event updated on "' + calendarData[calendarId].name + '" calendar',
                });
              })
              .catch(function (error) {
                inst.updateEvent(args.oldEvent);
                onError(error);
              });
          } else {
            inst.updateEvent(args.oldEvent);
          }
        });
    }
  },
  onEventDelete: function (args) {
    if (googleCalendarSync.isSignedIn()) {
      mobiscroll
        .confirm({
          title: 'Are you sure you want to delete this event?',
          message: 'This action will remove the event from your Google Calendar as well.',
          okText: 'Delete',
        })
        .then(function (result) {
          if (result) {
            var event = args.event;
            var calendarId = event.googleCalendarId;
            googleCalendarSync
              .deleteEvent(calendarId, event)
              .then(function () {
                inst.removeEvent(event);
                events = events.filter(function (ev) {
                  return event.id !== ev.id;
                });
                mobiscroll.toast({
                  message: 'Event deleted from "' + calendarData[calendarId].name + '" calendar',
                });
              })
              .catch(onError);
          }
        })
        .catch(onError);
    }
    return false;
  },
});

// Sign in
loginButton.addEventListener('click', function () {
  if (!googleCalendarSync.isSignedIn()) {
    googleCalendarSync.signIn().catch(onError);
  }
});

// Switch click
loggedInCont.addEventListener('change', function (ev) {
  if (ev.target.classList.contains('google-calendar-switch')) {
    loadEvents(ev.target.checked, ev.target.value);
  }
});

// Edit click
editButton.addEventListener('change', function (ev) {
  var isEditable = ev.target.checked;
  inst.setOptions({
    clickToCreate: isEditable,
    dragToCreate: isEditable,
    dragToMove: isEditable,
    dragToResize: isEditable,
  });
});

// Sign out
logoutButton.addEventListener('click', function () {
  googleCalendarSync.signOut().catch(onError);
});

document.querySelector('.md-sync-events-google-menu').classList.remove('mbsc-hidden');

// Init google client
googleCalendarSync.init({
  apiKey: '<YOUR_GOOGLE_API_KEY>',
  clientId: '<YOUR_GOOGLE_CLIENT_ID>',
  onSignedIn: onSignedIn,
  onSignedOut: onSignedOut,
});
```

### HTML

```html
<div mbsc-page class="md-sync-events-google-cont">
    <div class="md-sync-events-google-menu mbsc-hidden">
        <div aria-hidden="true" id="demo-logged-out-cont">
            <div class="mbsc-form-group-inset mbsc-align-center">
                <p class="mbsc-italic mbsc-txt-muted">Log into your Google account to view and edit your Google Calendar events</p>
                <button id="demo-google-cal-sign-in" class="mbsc-reset md-sync-events-google-button">Sign in with Google</button>
            </div>
        </div>
        <div aria-hidden="true" id="demo-logged-in-cont" style="display:none">
            <div class="mbsc-form-group-inset mbsc-align-center">
                <p class="mbsc-italic mbsc-txt-muted">Editing events sync back to your calendar when enabled. You'll be asked for confirmation on every action.</p>
            </div>
            <div class="mbsc-form-group-inset">
                <label>
                    <input id="demo-google-cal-edit" mbsc-switch data-label="Enable editing" />
                </label>
            </div>
            <div id="demo-google-cal-list" class="mbsc-form-group-inset md-sync-events-google-inset"></div>
            <div class="mbsc-form-group-inset">
                <button mbsc-button id="demo-google-cal-sign-out" class="mbsc-button-block">Log out of my account</button>
            </div>
        </div>
    </div>
    <div id="demo-google-calendar-cont" class="md-sync-events-google-calendar">
        <div class="md-sync-events-overlay"></div>
        <div id="demo-google-calendar"></div>
    </div>
</div>
```

### CSS

```css
.md-sync-events-google-cont.mbsc-page {
    display: flex;
    height: 600px;
}

.md-sync-events-google-menu {
    flex: 0 0 350px;
    height: 100%;
    overflow: auto;
}

.md-sync-events-google-calendar {
    flex: 1 1 auto;
    border-left: 1px solid #ccc;
}

.md-google-calendar-header {
    flex: 1 0 auto;
    display: flex;
    justify-content: flex-end;
}

button.md-sync-events-google-button {
    padding: 8px 24px 8px 50px;
    line-height: 24px;
    background: url(https://cdn.cdnlogo.com/logos/g/35/google-icon.svg) no-repeat;
    background-size: 18px 18px;
    background-position: 11px 11px;
    background-color: #fff;
    color: #757575;
    font-family: Roboto, arial, sans-serif;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    border-radius: 2px;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .25);
    transition: background-color .218s, border-color .218s, box-shadow .218s;
}

.md-sync-events-google-button:active {
    background-color: #eee;
    color: #6d6d6d;
}

.md-sync-events-google-button:hover,
.md-sync-events-google-button:focus {
    box-shadow: 0 0 3px 3px rgb(66, 133, 244, .3);
}

.md-sync-events-google-inset {
    margin-bottom: 0;
}

@media (max-width: 800px) {
    .md-sync-events-google-cont.mbsc-page {
        display: block;
        height: auto;
    }
}

/* loading spinner and overlay */

.md-loading-events .md-sync-events-overlay {
    position: absolute;
    z-index: 2;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    -webkit-transform: translateZ(0);
    transform: translateZ(0);
}

.md-spinner {
    visibility: hidden;
    position: relative;
    width: 20px;
    height: 20px;
}

.md-loading-events .md-spinner {
    visibility: visible;
}

.md-spinner .md-spinner-blade {
    position: absolute;
    left: 44.5%;
    top: 37%;
    width: 10%;
    height: 25%;
    border-radius: 50%/20%;
    background-color: #8C8C8C;
    -webkit-animation: md-spinner-fade 1s linear infinite;
    animation: md-spinner-fade 1s linear infinite;
    -webkit-animation-play-state: paused;
    animation-play-state: paused;
}

.md-spinner .md-spinner-blade:nth-child(1) {
    -webkit-animation-delay: -1.66667s;
    animation-delay: -1.66667s;
    -webkit-transform: rotate(30deg) translate(0, -150%);
    transform: rotate(30deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(2) {
    -webkit-animation-delay: -1.58333s;
    animation-delay: -1.58333s;
    -webkit-transform: rotate(60deg) translate(0, -150%);
    transform: rotate(60deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(3) {
    -webkit-animation-delay: -1.5s;
    animation-delay: -1.5s;
    -webkit-transform: rotate(90deg) translate(0, -150%);
    transform: rotate(90deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(4) {
    -webkit-animation-delay: -1.41667s;
    animation-delay: -1.41667s;
    -webkit-transform: rotate(120deg) translate(0, -150%);
    transform: rotate(120deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(5) {
    -webkit-animation-delay: -1.33333s;
    animation-delay: -1.33333s;
    -webkit-transform: rotate(150deg) translate(0, -150%);
    transform: rotate(150deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(6) {
    -webkit-animation-delay: -1.25s;
    animation-delay: -1.25s;
    -webkit-transform: rotate(180deg) translate(0, -150%);
    transform: rotate(180deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(7) {
    -webkit-animation-delay: -1.16667s;
    animation-delay: -1.16667s;
    -webkit-transform: rotate(210deg) translate(0, -150%);
    transform: rotate(210deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(8) {
    -webkit-animation-delay: -1.08333s;
    animation-delay: -1.08333s;
    -webkit-transform: rotate(240deg) translate(0, -150%);
    transform: rotate(240deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(9) {
    -webkit-animation-delay: -1s;
    animation-delay: -1s;
    -webkit-transform: rotate(270deg) translate(0, -150%);
    transform: rotate(270deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(10) {
    -webkit-animation-delay: -0.91667s;
    animation-delay: -0.91667s;
    -webkit-transform: rotate(300deg) translate(0, -150%);
    transform: rotate(300deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(11) {
    -webkit-animation-delay: -0.83333s;
    animation-delay: -0.83333s;
    -webkit-transform: rotate(330deg) translate(0, -150%);
    transform: rotate(330deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(12) {
    -webkit-animation-delay: -0.75s;
    animation-delay: -0.75s;
    -webkit-transform: rotate(360deg) translate(0, -150%);
    transform: rotate(360deg) translate(0, -150%);
}

.md-loading-events .md-spinner-blade {
    -webkit-animation-play-state: running;
    animation-play-state: running;
}

@-webkit-keyframes md-spinner-fade {
    0% {
        opacity: 0.85;
    }
    50% {
        opacity: 0.25;
    }
    100% {
        opacity: 0.25;
    }
}

@keyframes md-spinner-fade {
    0% {
        opacity: 0.85;
    }
    50% {
        opacity: 0.25;
    }
    100% {
        opacity: 0.25;
    }
}
```

## Implementation notes

- Install `@mobiscroll/calendar-integration` and import `googleCalendarSync`. Call `googleCalendarSync.init({ apiKey: '<YOUR_GOOGLE_API_KEY>', clientId: '<YOUR_GOOGLE_CLIENT_ID>', onSignedIn, onSignedOut })` on component mount.
- Use `view: { calendar: { labels: true } }` with `exclusiveEndDates: true`. Load events on each page navigation via `onPageLoading` (debounced ~200 ms): call `googleCalendarSync.getEvents(calendarIds, viewStart, viewEnd)` and update the events array.
- Layout: `Page` wrapper with a left side panel (auth/calendar controls) and right calendar area. **Signed-out** panel: explanatory text + "Sign in with Google" button (calls `googleCalendarSync.signIn()`). **Signed-in** panel: usage note, "Enable editing" `Switch`, "My Calendars" section with one `Switch` per calendar (toggle fires `googleCalendarSync.getEvents()` for the current range), and "Log out" button (calls `googleCalendarSync.signOut()`).
- On `onSignedIn`: call `googleCalendarSync.getCalendars()`, sort to put primary calendar first, build a `calendarData` map keyed by calendar ID, set `calendarIds` to `[primaryCalendarId]`, then fetch events for the current view range.
- CRUD - all gated by the `editable` switch and `googleCalendarSync.isSignedIn()`: **create** calls `googleCalendarSync.addEvent(primaryCalendarId, event)` in `onEventCreate`; **update** calls `googleCalendarSync.updateEvent(calendarId, event)` in `onEventUpdate` after a `Confirm` dialog; **delete** calls `googleCalendarSync.deleteEvent(calendarId, event)` in `onEventDelete` after a `Confirm` dialog. On API failure, revert the events array to the previous state. Show a `Toast` on each success or error. Pass `clickToCreate`, `dragToCreate`, `dragToMove`, `dragToResize` all bound to the `editable` flag. Set `extendDefaultEvent` to assign the primary calendar's color to newly created events. Use `renderHeader` (Angular: `headerTemplate`, Vue: `#header` slot) to show a CSS-animated loading spinner alongside `CalendarNav`, `CalendarPrev`, `CalendarToday`, `CalendarNext`.

## Run it in your project

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/javascript/getting-started/installation

## Related demos

- Next: [Outlook Integration](https://demo.mobiscroll.com/javascript/eventcalendar/sync-events-outlook-calendar.md) - Connect the Javascript event calendar to Outlook via plugin — sign in users, load calendars, and add, edit, or delete events with optional write-back.
