---
title: "Angular team meeting planner across timezones | Mobiscroll"
component: "Timeline"
framework: "Angular"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/angular/timeline/timezone-meeting-planner"
---

# Timezone Meeting Planner - Angular Timeline demo

Plan team meetings across time zones with the Angular Timeline — show per-resource availability in working, flex, and time-off blocks, with local-time events. For Angular 22 and Ionic 8.

- [Live demo](https://demo.mobiscroll.com/angular/timeline/timezone-meeting-planner)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-angular/tree/main/src/demos/eventcalendar/timeline/timezone-meeting-planner)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/timezone-meeting-planner.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/timezone-meeting-planner.md), [React](https://demo.mobiscroll.com/react/timeline/timezone-meeting-planner.md), [Vue](https://demo.mobiscroll.com/vue/timeline/timezone-meeting-planner.md)

## Description

Setting up meetings across multiple timezones is messy and hard task. In this example, built with the Mobiscroll scheduler we've laid out a timeline with multiple resources that represent the teammates. Each team member can be in separate timezones which means we need to account for the various offsets.

First we need to set a base timezone, which for the sake of simplicity is `UTC` and it is the timezone of the first resource, the meeting organizer. This also means that the scheduler operates in this timezone set through the [`dataTimezone`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dataTimezone) and [`displayTimezone`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-displayTimezone) options. To learn more about timezone management [take a look at this example](https://demo.mobiscroll.com/angular/timeline/setting-the-timezone).

Because all resources are part of the same calendar they operate in the same timezone. That's why we need to somehow communicate the work hours with the correct offsets so that the person doing the scheduling can easily find the best times for the meeting. For that we are setting [colored backgrounds](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-colors) with time labels that show the "working hours", "flex hours" and "time off". Besides the colored backgrounds we've also set "time off" to [invalid](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-invalid) for validation purposes. On how to work with disabled times and how to override them [take a look at this example](https://demo.mobiscroll.com/angular/scheduler/time-off-blocked-ranges).

The last thing that needed to be done is to override the event rendering with a custom template. In the custom template/render function we can account for the offsets and print the times in the team members timezone instead of the base timezone (UTC in this example). [Learn how to create a custom event template.](https://demo.mobiscroll.com/angular/timeline/timeline-custom-event-rendering)

## What this demo shows

- A desktop timeline for planning meetings across multiple time zones, with days arranged horizontally and resources listed vertically.
- **Header navigation** The month and year label in the top left opens date navigation, while the blue previous and next arrows and the Today button on the right make it easy to move between weeks and jump back to the current day.
- **Legend** A color legend explains the time-period backgrounds: yellow for working hours, blue for flex hours, and red for time off.
- **Week view** The fixed date strip shows the selected week, with date labels formatted as `DD DDD MMM YYYY` and the current date highlighted.
- **Resources** Six team members are shown as timeline rows, each with an avatar, name, and local time zone information.
- **Hourly timeline** The timeline shows 24 hourly slots, with the hour labels adjusted per resource so local times can be compared across multiple time zones.
- **Availability backgrounds** Timeline cells use background colors to show each resource's working hours, flex hours, and time-off periods.
- **Current time** A vertical blue line marks the current time and includes the current time label.
- **Event cards** Meeting events are rendered as green cards with the localized start and end time above the bold event title.
- **Resource placement** Events are positioned by date, time range, and assigned resource, with meetings spanning all resources by default.
- **Event interaction** Events highlight on hover and expose drag and resize handles for moving the meeting or changing its duration.
- **Event creation** New events can be created directly from the timeline with click-to-create and drag-to-create interactions.
- **Time-off validation** When a new or updated event overlaps with a time-off period for one of the resources, a confirmation dialog asks whether to proceed.
- **Horizontal scrolling** The timeline supports horizontal scrolling for navigating through the visible date and time range.

## Best for

- **Global workforce scheduling** Planning meetings and shared work sessions across teams in different time zones.
- **Distributed teams** Comparing participant availability when each person works from a different location.
- **International support organizations** Coordinating coverage and collaboration windows across regions.
- **Cross-time-zone resource planning** Finding meeting times that respect working hours, flex hours, and time-off periods.
- **Team meeting planning** Scheduling group events that include every selected participant by default.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`invalid`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-invalid) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-resources) - option
- [`timezonePlugin`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-timezonePlugin) - option
- [`dataTimezone`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dataTimezone) - option
- [`displayTimezone`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-displayTimezone) - option
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`dragTimeStep`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragTimeStep) - option
- [`onEventCreated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreated) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDeleted) - event
- [`onEventCreateFailed`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreateFailed) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdateFailed) - event
- [`extendDefaultEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-extendDefaultEvent) - option
- [`timelineEventTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-timelineEventTemplate) - template
- [`headerTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-headerTemplate) - template
- [`resourceTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceTemplate) - template
- [`cellTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-cellTemplate) - template

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
  dayjsTimezone,
  formatDate,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscModule,
  MbscTimezonePlugin,
  Notifications,
  setOptions,
} from '@mobiscroll/angular';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjsTimezone.dayjs = dayjs;

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  providers: [Notifications],
  standalone: true,
  imports: [CommonModule, MbscModule],
})
export class AppComponent {
  constructor(private notify: Notifications) {}
  myEvents: MbscCalendarEvent[] = [
    {
      start: '2026-10-22T13:00',
      end: '2026-10-22T15:00',
      title: 'General orientation',
      color: '#1ad404',
      resource: [1, 2, 3, 4, 5, 6],
    },
  ];
  myResources = [
    {
      id: 1,
      name: 'Keila Delores',
      timezone: 'utc',
      img: 'https://img.mobiscroll.com/demos/f1.png',
      utcOffset: '(UTC)',
      organizer: true,
    },
    {
      id: 2,
      name: 'Gene Cortez',
      timezone: 'America/Chicago',
      img: 'https://img.mobiscroll.com/demos/m1.png',
      utcOffset: 'UTC - 5',
    },
    {
      id: 3,
      name: 'Paula Bush',
      timezone: 'America/New_York',
      img: 'https://img.mobiscroll.com/demos/f2.png',
      utcOffset: 'UTC - 4',
    },
    {
      id: 4,
      name: 'Pete Nichols',
      timezone: 'Europe/London',
      img: 'https://img.mobiscroll.com/demos/m2.png',
      utcOffset: 'UTC + 1',
    },
    {
      id: 5,
      name: 'Jean Pearson',
      timezone: 'Europe/Berlin',
      img: 'https://img.mobiscroll.com/demos/m3.png',
      utcOffset: 'UTC + 2',
    },
    {
      id: 6,
      name: 'Thelma Cain',
      timezone: 'Europe/Bucharest',
      img: 'https://img.mobiscroll.com/demos/f3.png',
      utcOffset: 'UTC + 3',
    },
  ];

  myView: MbscEventcalendarView = {
    timeline: {
      type: 'week',
      timeLabelStep: 1440,
    },
  };

  myTimezonePlugin: MbscTimezonePlugin = dayjsTimezone;
  myDefaultEvent(): MbscCalendarEvent {
    return { resource: [1, 2, 3, 4, 5, 6] };
  }
  myInvalid: MbscCalendarEvent[] = this.getInvalids();

  getHourProps(h: number, timezone: string) {
    const offset = this.getUtcOffset(timezone);
    const hour = h + offset;
    const isAM = hour % 24 < 12;
    const title = ((hour % 12) + 12) % 12 || 12;
    const hForProps = title + ((title === 12 && !isAM) || (title !== 12 && isAM) ? 0 : 12);
    let color = '#f7f7bb4d';
    let invalid = false;

    if (hForProps < 6 || hForProps >= 22) {
      color = '#ffbaba4d';
      invalid = true;
    } else if (hForProps < 8 || (hForProps >= 18 && hForProps < 22)) {
      color = '#a5ceff4d';
    }

    return {
      hour: hour,
      isAM: isAM,
      title: title,
      color: color,
      invalid: invalid,
    };
  }

  getInvalids(): MbscCalendarEvent[] {
    const invalid: MbscCalendarEvent[] = [];

    for (const resource of this.myResources) {
      for (let i = 0; i < 24; ++i) {
        if (this.getHourProps(i, resource.timezone).invalid) {
          const startTime = (i < 10 ? '0' : '') + i + ':00:00';
          const endTime = (i < 9 ? '0' : '') + (i + 1) + ':00:00';

          invalid.push({
            start: startTime,
            end: endTime,
            resource: resource.id,
            recurring: {
              repeat: 'daily',
            },
          });
        }
      }
    }
    return invalid;
  }

  onEventCreated(args: MbscEventCreatedEvent): void {
    setTimeout(() => {
      this.myEvents = [...this.myEvents, args.event];
      this.notify.toast({
        message: 'Event created',
      });
    });
  }

  onEventUpdated(args: MbscEventUpdatedEvent): void {
    setTimeout(() => {
      const index = this.myEvents.findIndex((x) => x.id === args.event.id);
      const newEventList = [...this.myEvents];

      newEventList.splice(index, 1, args.event);
      this.myEvents = newEventList;
      this.notify.toast({
        message: 'Event updated',
      });
    });
  }

  onEventDeleted(args: MbscEventDeletedEvent): void {
    setTimeout(() => {
      this.myEvents = this.myEvents.filter((item) => item.id !== args.event.id);
    });
  }

  createUpdateEvent(event: any, isNew: boolean): void {
    this.notify.confirm({
      title: 'Are you sure you want to proceed?',
      message: "It looks like someone from the team won't be able to join the meeting.",
      okText: 'Yes',
      cancelText: 'No',
      callback: (res) => {
        if (res) {
          if (isNew) {
            this.myEvents = [...this.myEvents, event];
          } else {
            const index = this.myEvents.findIndex((x) => x.id === event.id);
            const newEventList = [...this.myEvents];

            newEventList.splice(index, 1, event);
            this.myEvents = newEventList;
          }

          this.notify.toast({
            message: isNew ? 'Event created' : 'Event updated',
          });
        }
      },
    });
  }

  getUtcOffset(timezone: string): number {
    switch (timezone) {
      case 'America/Los_Angeles':
        return -7;
      case 'America/Chicago':
        return -5;
      case 'America/New_York':
        return -4;
      case 'Europe/London':
        return 1;
      case 'Europe/Berlin':
        return 2;
      case 'Europe/Bucharest':
        return 3;
      case 'Asia/Shanghai':
        return 8;
      case 'Asia/Tokyo':
        return 9;
      default:
        return 0;
    }
  }

  getTitleTime(data: any): string {
    const start = data.startDate.clone();
    const end = data.endDate.clone();

    start.setTimezone(data.currentResource.timezone);
    end.setTimezone(data.currentResource.timezone);

    return formatDate('hh:mm A', start) + ' - ' + formatDate('hh:mm A', end);
  }
}
```

### Template

```html
<mbsc-eventcalendar
  [view]="myView"
  [data]="myEvents"
  [resources]="myResources"
  [invalid]="myInvalid"
  [timezonePlugin]="myTimezonePlugin"
  [dataTimezone]="'utc'"
  [displayTimezone]="'utc'"
  [clickToCreate]="true"
  [dragToCreate]="true"
  [dragToMove]="true"
  [dragToResize]="true"
  [dragTimeStep]="60"
  (onEventCreated)="onEventCreated($event)"
  (onEventUpdated)="onEventUpdated($event)"
  (onEventDeleted)="onEventDeleted($event)"
  (onEventCreateFailed)="createUpdateEvent($event.event, true)"
  (onEventUpdateFailed)="createUpdateEvent($event.event, false)"
  [extendDefaultEvent]="myDefaultEvent"
  [timelineEventTemplate]="myEventTemplate"
  [headerTemplate]="myHeaderTemplate"
  [resourceTemplate]="myResourceTemplate"
  [cellTemplate]="myCellTemplate"
>
  <ng-template #myEventTemplate let-data>
    <div class="mds-meeting-planner-cont" [ngStyle]="{background: data.color}">
      <div class="mds-meeting-planner-wrapper">
        <div class="mds-meeting-planner-title">{{data.title}}</div>
        <div class="mds-meeting-planner-time">{{getTitleTime(data)}}</div>
      </div>
    </div>
  </ng-template>
  <ng-template #myHeaderTemplate>
    <mbsc-calendar-nav class="mds-timezone-nav"></mbsc-calendar-nav>
    <div class="mds-meeting-planner-header">
      <div class="mds-meeting-planner-zone mds-meeting-planner-work">working hours</div>
      <div class="mds-meeting-planner-zone mds-meeting-planner-flex">flex hours</div>
      <div class="mds-meeting-planner-zone mds-meeting-planner-off">time off</div>
      <mbsc-calendar-prev class="mds-work-week-prev"></mbsc-calendar-prev>
      <mbsc-calendar-today class="mds-work-week-today"></mbsc-calendar-today>
      <mbsc-calendar-next class="mds-work-week-next"></mbsc-calendar-next>
    </div>
  </ng-template>
  <ng-template #myResourceTemplate let-resource>
    <div class="mds-meeting-participant-cont">
      <div class="mds-meeting-participant-name">{{resource.name}}</div>
      <div>
        <span *ngIf="resource.organizer">Organizer </span>
        <span class="mds-meeting-participant-offset">{{resource.utcOffset}}</span>
      </div>
      <img class="mds-meeting-participant-avatar" [attr.alt]="resource.name" [src]="resource.img" />
    </div>
  </ng-template>
  <ng-template #myCellTemplate let-args>
    <div
      class="mds-meeting-planner-time-slot mbsc-flex mbsc-justify-content-center"
      [ngStyle]="{'background-color': hProps.color}"
      *ngIf="getHourProps(args.date.getHours(), args.resource.timezone) as hProps"
    >
      {{hProps.title}} {{hProps.isAM ? ' AM' : ' PM'}}
    </div>
  </ng-template>
</mbsc-eventcalendar>
```

### CSS

```css
.mds-meeting-planner-cont {
  font-size: 12px;
  font-weight: 600;
  height: 100%;
  background: #1ad404;
  position: relative;
  box-sizing: border-box;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
  border-radius: 6px;
  overflow: hidden;
}

.mds-meeting-planner-wrapper {
  background: rgba(255, 255, 255, 0.5);
  height: 100%;
  box-sizing: border-box;
  padding: 0 6px;
  transition: background 0.15s ease-in-out;
}

.mbsc-schedule-event-hover .mds-meeting-planner-wrapper {
  background: rgba(255, 255, 255, 0.3);
}

.mds-meeting-planner-title {
  padding-top: 3px;
  color: initial;
}

.mds-meeting-planner-time {
  color: #666;
}

.mds-meeting-planner-title,
.mds-meeting-planner-time {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mds-meeting-planner-header {
  display: flex;
  align-items: center;
  margin-left: auto;
}

.mds-meeting-planner-zone {
  font-size: 12px;
  padding: 3px 6px;
  margin: 0 5px;
  border-radius: 16px;
  color: #888;
}

.mds-meeting-planner-work {
  background: #f7f7bb4d;
}

.mds-meeting-planner-flex {
  background: #a5ceff4d;
}

.mds-meeting-planner-off {
  background: #ffbaba4d;
}

.mds-meeting-participant-cont {
  position: relative;
  padding-left: 50px;
  max-height: 40px;
  line-height: 20px;
}

.mds-meeting-participant-avatar {
  position: absolute;
  max-height: 40px;
  max-width: 40px;
  top: 18px;
  -webkit-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
  left: 20px;
}

.mds-meeting-participant-name {
  font-size: 16px;
}

.mds-meeting-participant-offset {
  font-size: 12px;
  opacity: 0.6;
}

.mds-meeting-planner-time-slot {
  font-size: 12px;
  opacity: 0.7;
  line-height: 50px;
  height: 100%;
  pointer-events: none;
}
```

## Implementation notes

- Use the Mobiscroll `dayjsTimezone` plugin together with `dayjs`, `dayjs/plugin/timezone`, and `dayjs/plugin/utc`, and pass `timezonePlugin` to the Eventcalendar.
- Set `dataTimezone` and `displayTimezone` to `utc` to anchor all event data and display in the organizer's base timezone.
- Use `type: 'week'` with `timeLabelStep: 1440` so the time axis shows one date label per day rather than per-hour labels.
- Give each resource a custom `timezone` property (IANA string) and a display `utcOffset` string, and mark the meeting organizer with a custom flag.
- Write a helper that converts a UTC hour and a resource timezone to the corresponding local hour and returns a background color and an `invalid` flag: 22:00-6:00 local is time off (red, invalid), 6:00-8:00 and 18:00-22:00 are flex hours (blue), 8:00-18:00 are working hours (yellow).
- Derive the `invalid` array from the resource list by iterating over all 24 hours per resource and collecting the time-off slots, each marked with `recurring: { repeat: 'daily' }`.
- Use `renderCell` (Angular: `cellTemplate`, Vue: `#cell` slot) to render each timeline cell with the local hour label and the background color from the hour classification helper.
- Use `extendDefaultEvent` to pre-assign all resource IDs to every new event so meetings span all team members by default.
- Use `renderTimelineEvent` (Angular: `timelineEventTemplate`, Vue: `#timelineEvent` slot) to show localized times per row: call `setTimezone` on the start and end dates with `data.currentResource.timezone` and format the result.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to display the participant's name, UTC offset, and an organizer label.
- Use `renderHeader` (Angular: `headerTemplate`, Vue: `#header` slot) to render the navigation controls alongside a color legend for working hours, flex hours, and time off.
- Enable `clickToCreate`, `dragToCreate`, `dragToMove`, and `dragToResize` with `dragTimeStep: 60` for one-hour snapping.
- Use `onEventCreateFailed` and `onEventUpdateFailed` to open a `Confirm` dialog when a meeting lands in a time-off slot, and proceed with the create or update if the user confirms.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-angular.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config angular
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   ng serve
   ```

On a trial, add `--trial` to the config command: `mobiscroll config angular --trial`.

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

## Related demos

- Previous: [Work Order Scheduling](https://demo.mobiscroll.com/angular/timeline/work-order-scheduling.md) - Schedule work orders across grouped resources — drivers, trucks, and contractors — on a Angular timeline.
- Next: [Restaurant Shift Scheduling](https://demo.mobiscroll.com/angular/timeline/restaurant-shift-management.md) - Build a restaurant shift planning interface with the Angular Timeline — named shift columns with live occupancy counts and hierarchical staff groups by role.
