---
title: "Javascript Timeline Employee Shift Scheduling Example | Mobiscroll"
component: "Timeline"
framework: "JavaScript"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/javascript/timeline/employee-shifts"
---

# Employee Shift Scheduling - JavaScript Timeline demo

Plan employee shifts on a Javascript Timeline — show workers as resource rows with morning and afternoon slots, and manage shifts with a custom add/edit dialog. Plain JavaScript API.

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

## Description

Use the summary mode of the timeline view with `eventDisplay: 'fill'` that can be configured under the `timeline` settings of the [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) option.

This will give you summarized daily events similar to the [event calendar labels](https://demo.mobiscroll.com/javascript/eventcalendar/event-labels) where each event is printed under the appropriate day, time slot (shift) for the appropriate resource one after the other.

Build a custom add/edit dialog with the necessary fields. The event dialog can be fully custom as seen in this [CRUD example](https://demo.mobiscroll.com/javascript/scheduler/create-read-update-delete-CRUD).

Customize the dialog header with a custom header renderer function using the [`renderHeader`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderHeader) option.

## What this demo shows

- A weekly timeline-based shift planner that displays employees as resource rows and shows only work days.
- **Shift structure** Each day is divided into two named slots, Morning and Afternoon, instead of showing shifts by exact position on a time grid.
- **Resources** Each row represents an employee, and the events shown in that row are the scheduled shifts for that employee.
- **Visual distinction** Each employee has an assigned color, and their shifts use that color in the timeline.
- **Editing shifts** Clicking an existing shift opens a popup with an edit form where the shift start and end times can be changed and notes can be added.
- **Deleting shifts** The edit popup includes a delete action that removes the shift immediately, then shows a snackbar confirmation with an Undo action to restore it.
- **Creating shifts** Double-clicking an empty timeline slot opens a popup for adding a new shift.
- **Add form** The create popup lets users set the shift start and end times and add notes before saving.

## Best for

- **Employee shift planning** Weekly rota and shift scheduling interfaces where employees are shown as resources.
- **Work-week scheduling** Setups that focus on working days only rather than a full seven-day timeline.
- **Named-shift layouts** Scenarios where shifts are organized into fixed slots such as Morning and Afternoon instead of being positioned by exact time.
- **Color-coded teams** Calendars where resource-based color coding helps users quickly identify which shifts belong to which employee.
- **Direct calendar editing** Applications where users need to create, update, delete, and restore shifts directly from the calendar UI.

## 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
- [`setOptions`](https://mobiscroll.com/docs/javascript/datepicker/api#method-setOptions) - method
- [`minTime`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-minTime) - option
- [`maxTime`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-maxTime) - option
- [`updateEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-updateEvent) - method
- [`close`](https://mobiscroll.com/docs/javascript/popup/api#method-close) - method
- [`setOptions`](https://mobiscroll.com/docs/javascript/popup/api#method-setOptions) - method
- [`buttons`](https://mobiscroll.com/docs/javascript/popup/api#opt-buttons) - option
- [`renderHeader`](https://mobiscroll.com/docs/javascript/popup/api#renderer-renderHeader) - renderer
- [`onClose`](https://mobiscroll.com/docs/javascript/popup/api#event-onClose) - event
- [`removeEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-removeEvent) - method
- [`open`](https://mobiscroll.com/docs/javascript/popup/api#method-open) - method
- [`cssClass`](https://mobiscroll.com/docs/javascript/popup/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToMove) - option
- [`data`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-data) - option
- [`eventOverlap`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-eventOverlap) - option
- [`invalid`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-invalid) - option
- [`resources`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-resources) - option
- [`slots`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-slots) - option
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-extendDefaultEvent) - option
- [`renderResource`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderResource) - renderer
- [`onEventClick`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated) - event
- [`onEventCreateFailed`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreateFailed) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdated) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdateFailed) - event
- [`display`](https://mobiscroll.com/docs/javascript/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/javascript/popup/api#opt-fullScreen) - option
- [`responsive`](https://mobiscroll.com/docs/javascript/popup/api#opt-responsive) - option
- [`touchUi`](https://mobiscroll.com/docs/javascript/popup/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/javascript/popup/api#opt-width) - option
- [`scrollLock`](https://mobiscroll.com/docs/javascript/popup/api#opt-scrollLock) - option
- [`onOpen`](https://mobiscroll.com/docs/javascript/popup/api#event-onOpen) - event
- [`controls`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-controls) - option
- [`display`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-display) - option
- [`select`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-select) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-showRangeLabels) - option
- [`stepMinute`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-stepMinute) - option
- [`startInput`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-endInput) - option
- [`timeWheels`](https://mobiscroll.com/docs/javascript/datepicker/api#localization-timeWheels) - option
- [`touchUi`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-touchUi) - option
- [`addEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-addEvent) - method

## Code

### JS

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

function getShiftTimes(event) {
  var d = event.start;
  var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), event.slot == 1 ? 7 : 12);
  var end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), event.slot == 1 ? 13 : 18);

  return {
    title: formatDate('HH:mm', start) + ' - ' + formatDate('HH:mm', end),
    start: start,
    end: end,
  };
}

function fillPopup(event) {
  mobiscroll.getInst(shiftNotesElm).value = event.notes || '';
  eventStartEndPicker.setOptions({ minTime: event.slot == 1 ? '07:00' : '12:00', maxTime: event.slot == 1 ? '13:00' : '18:00' });
  eventStartEndPicker.setVal([new Date(event.start), new Date(event.end)]);
}

function saveEvent(event) {
  var dates = eventStartEndPicker.getVal();
  var start = dates[0];
  var end = dates[1] ? dates[1] : dates[0];

  var shiftStart = new Date(event.start);
  var shiftEnd = new Date(event.end);

  shiftStart.setHours(start.getHours(), start.getMinutes(), 0, 0);
  shiftEnd.setHours(end.getHours(), end.getMinutes(), 0, 0);

  event.start = shiftStart;
  event.end = shiftEnd;
  event.title = formatDate('HH:mm', start) + ' - ' + formatDate('HH:mm', end);
  event.notes = shiftNotesElm.value;

  calendar.updateEvent(event);
  addEditPopup.close();
}

function createAddPopup(args) {
  var event = args.event;
  var success = false;

  var slot = args.slotObj;

  // Hide delete button inside add popup
  shiftDeleteButton.parentElement.style.display = 'none';

  shift = event;

  // Set popup header text and buttons
  addEditPopup.setOptions({
    buttons: [
      'cancel',
      {
        text: 'Add',
        keyCode: 'enter',
        handler: function () {
          success = true;
          saveEvent(event);
        },
        addEditPopup: 'mbsc-popup-button-primary',
      },
    ],
    renderHeader: function () {
      return (
        '<div class="mds-employee-shifts-header-primary">New shift</div>' +
        '<div class="mds-employee-shifts-header-date">' +
        formatDate('DDDD', new Date(event.start)) +
        ' ' +
        slot.name +
        ', ' +
        formatDate('D MMMM YYYY', new Date(event.start)) +
        '</div>'
      );
    },
    onClose: function () {
      // Remove event if popup is cancelled
      if (!success) {
        calendar.removeEvent(event);
      }
    },
  });

  addEditPopup.open();
}

function createEditPopup(args) {
  var event = args.event;
  // Show delete button inside edit popup
  shiftDeleteButton.parentElement.style.display = 'block';

  shift = event;

  var resource = args.resourceObj;

  var slot = args.slotObj;

  addEditPopup.setOptions({
    buttons: [
      'cancel',
      {
        text: 'Save',
        keyCode: 'enter',
        handler: function () {
          saveEvent(event);
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
    renderHeader: function () {
      return (
        '<div class="mds-employee-shifts-header-primary">Edit ' +
        resource.name +
        "'s hours</div>" +
        '<div class="mds-employee-shifts-header-date">' +
        formatDate('DDDD', new Date(event.start)) +
        ' ' +
        slot.name +
        ', ' +
        formatDate('D MMMM YYYY', new Date(event.start)) +
        '</div>'
      );
    },
  });

  addEditPopup.open();
}

var formatDate = mobiscroll.formatDate;

var shift;
var shiftNotesElm = document.getElementById('demo-popup-shift-notes');
var shiftDeleteButton = document.getElementById('demo-popup-shift-delete');

var staff = [
  {
    id: 1,
    name: 'Ryan',
    color: '#e20000',
    title: 'Cloud System Engineer',
    img: 'https://img.mobiscroll.com/demos/m1.png',
  },
  {
    id: 2,
    name: 'Kate',
    color: '#60e81a',
    title: 'Help Desk Specialist',
    img: 'https://img.mobiscroll.com/demos/f1.png',
  },
  {
    id: 3,
    name: 'John',
    color: '#3ba7ff',
    title: 'Application Developer',
    img: 'https://img.mobiscroll.com/demos/m2.png',
  },
  {
    id: 4,
    name: 'Mark',
    color: '#e25dd2',
    title: 'Network Administrator',
    img: 'https://img.mobiscroll.com/demos/m3.png',
  },
  {
    id: 5,
    name: 'Sharon',
    color: '#f1e920',
    title: 'Data Quality Manager',
    img: 'https://img.mobiscroll.com/demos/f2.png',
  },
  {
    id: 6,
    name: 'Emma',
    color: '#1ac38d',
    title: 'Product Tactics Agent',
    img: 'https://img.mobiscroll.com/demos/f3.png',
  },
  {
    id: 7,
    name: 'Michael',
    color: '#f18a1a',
    title: 'UX Designer',
    img: 'https://img.mobiscroll.com/demos/m4.png',
  },
  {
    id: 8,
    name: 'Laura',
    color: '#7b61ff',
    title: 'DevOps Engineer',
    img: 'https://img.mobiscroll.com/demos/f4.png',
  },
];

var shifts = [
  {
    start: '2026-10-25T07:00',
    end: '2026-10-25T13:00',
    title: '07:00 - 13:00',
    resource: 1,
    slot: 1,
  },
  {
    start: '2026-10-25T07:00',
    end: '2026-10-25T13:00',
    title: '07:00 - 13:00',
    resource: 7,
    slot: 1,
  },
  {
    start: '2026-10-25T12:00',
    end: '2026-10-25T18:00',
    title: '12:00 - 18:00',
    resource: 3,
    slot: 2,
  },
  {
    start: '2026-10-25T12:00',
    end: '2026-10-25T18:00',
    title: '12:00 - 18:00',
    resource: 8,
    slot: 2,
  },
  {
    start: '2026-10-26T07:00',
    end: '2026-10-26T13:00',
    title: '07:00 - 13:00',
    resource: 4,
    slot: 1,
  },
  // Showing partial data. Download full source.
];

var mySlots = [
  { id: 1, name: 'Morning' },
  { id: 2, name: 'Afternoon' },
];

var myInvalids = [
  {
    start: '2026-10-30T00:00',
    end: '2026-10-30T23:59',
    resource: 4,
    slot: 1,
  },
  {
    start: '2026-10-28T00:00',
    end: '2026-10-28T23:59',
    resource: 2,
    slot: 2,
  },
];

var calendar = mobiscroll.eventcalendar('#demo-employee-shifts-calendar', {
  clickToCreate: true,
  dragToCreate: false,
  dragToResize: false,
  dragToMove: true,
  data: shifts,
  eventOverlap: false,
  invalid: myInvalids,
  resources: staff,
  slots: mySlots,
  view: {
    timeline: {
      type: 'week',
      eventDisplay: 'fill',
      startDay: 1,
      endDay: 5,
    },
  },
  extendDefaultEvent: getShiftTimes,
  renderResource: function (resource) {
    return (
      '<div class="mbsc-flex">' +
      '<img alt="' +
      resource.name +
      '" class="mds-employee-shifts-avatar" src="' +
      resource.img +
      '"/>' +
      '<div class="mds-employee-shifts-cont">' +
      '<div class="mds-employee-shifts-name">' +
      resource.name +
      '</div>' +
      '<div class="mds-employee-shifts-title">' +
      resource.title +
      '</div>' +
      '</div>' +
      '</div>'
    );
  },
  onEventClick: function (args) {
    createEditPopup(args);
  },
  onEventCreated: function (args) {
    createAddPopup(args);
  },
  onEventCreateFailed: function () {
    mobiscroll.toast({
      message: "Can't create shift",
    });
  },
  onEventUpdated: function (args) {
    var event = args.event;
    if (event.slot !== args.oldEvent.slot) {
      var data = getShiftTimes(event);
      event.start = data.start;
      event.end = data.end;
      event.title = data.title;
      calendar.updateEvent(event);
    }
  },
  onEventUpdateFailed: function () {
    mobiscroll.toast({
      message: "Can't move shift",
    });
  },
});

var addEditPopup = mobiscroll.popup('#demo-employee-shifts-popup', {
  display: 'bottom',
  contentPadding: false,
  fullScreen: true,
  responsive: {
    medium: {
      display: 'center',
      fullScreen: false,
      touchUi: false,
      width: 400,
    },
  },
  scrollLock: false,
  onOpen: function () {
    fillPopup(shift);
  },
});

var eventStartEndPicker = mobiscroll.datepicker('#demo-popup-shift-dates', {
  controls: ['time'],
  display: 'anchored',
  select: 'range',
  showRangeLabels: false,
  stepMinute: 30,
  startInput: '#demo-popup-shift-start',
  endInput: '#demo-popup-shift-end',
  timeWheels: '|h:mm A|',
  touchUi: false,
});

shiftDeleteButton.addEventListener('click', function () {
  calendar.removeEvent(shift);

  addEditPopup.close();

  // Save a local reference to the deleted event
  var deletedEvent = shift;

  mobiscroll.snackbar({
    button: {
      action: function () {
        calendar.addEvent(deletedEvent);
      },
      text: 'Undo',
    },
    message: 'Shift deleted',
  });
});
```

### HTML

```html
<div id="demo-employee-shifts-calendar" class="mds-employee-shifts"></div>

<div style="display: none;">
  <div id="demo-employee-shifts-popup">
    <div>
      <div id="demo-popup-shift-dates"></div>
    </div>
    <div class="mbsc-form-group">
      <label>
        <input mbsc-input data-label="Shift start" data-dropdown="true" id="demo-popup-shift-start" />
      </label>
      <label>
        <input mbsc-input data-label="Shift end" data-dropdown="true" id="demo-popup-shift-end" />
      </label>
    </div>
    <div class="mbsc-form-group">
      <label>
        <textarea mbsc-textarea data-label="Notes" id="demo-popup-shift-notes"></textarea>
      </label>
    </div>
    <div class="mbsc-button-group">
      <button class="mbsc-button-block" id="demo-popup-shift-delete" mbsc-button data-color="danger" data-variant="outline">Delete shift</button>
    </div>
  </div>
</div>
```

### CSS

```css
.mds-employee-shifts-header-primary {
  font-size: 16px;
  line-height: 20px;
}

.mds-employee-shifts-header-date {
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
  opacity: .6;
}

.mds-employee-shifts .mbsc-timeline-resource-col {
  width: 200px;
}

.mds-employee-shifts .mbsc-schedule-event-inner {
  text-align: center;
  height: 36px;
  line-height: 36px;
}

.mds-employee-shifts-cont {
  padding: 0 7px;
}

.mds-employee-shifts-name {
  font-size: 14px;
  line-height: 24px;
}

.mds-employee-shifts-title {
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
}

.mds-employee-shifts-avatar {
  width: 40px;
  height: 40px;
}
```

## Implementation notes

- Use the timeline view in summary mode with `eventDisplay: 'fill'` to render shift events within resource rows and named slot buckets. Set `startDay: 1` and `endDay: 5` to show only work days.
- Define the Morning and Afternoon shift buckets with the `slots` option. Each slot object needs an `id` and a `name`, and each event references its slot via the `slot` field.
- Give each employee a `color` property on the resource object. Mobiscroll automatically applies the resource color to its events, so no extra event-level styling is needed.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to display a custom resource row with the employee's avatar image, name, and job title.
- Enable `clickToCreate` so users can create a new shift by clicking an empty cell. Set `dragToMove: true` so existing shifts can be repositioned by dragging, and set `dragToCreate: false` and `dragToResize: false` to prevent accidental event creation by drag and to keep shift boundaries tied to the slot definition.
- Use `extendDefaultEvent` to set the correct start and end times on every newly created event based on which slot it lands in - for example, Morning maps to 07:00-13:00 and Afternoon to 12:00-18:00.
- Set `eventOverlap: false` to block placing a second shift for the same employee in the same slot on the same day.
- Use the `invalid` option to mark specific resource-slot-day combinations where shifts cannot be created or moved, such as an employee's day off or a slot they are not eligible for.
- Use `onEventCreated` to intercept the newly created event, populate the popup with the slot-derived time range, and open the add form. The `slotObj` in the event args provides the slot name for the popup header.
- Use `onEventClick` to open the edit form for an existing shift. Read `resourceObj` and `slotObj` from the event args to build a descriptive popup header showing the employee name, day, and slot.
- Use `onEventUpdated` to detect when a shift has been dragged from one slot to another. When the slot changes, recalculate and overwrite the event's start, end, and title to match the new slot's fixed time range.
- Use `onEventCreateFailed` and `onEventUpdateFailed` to show a toast message when a shift cannot be placed or moved, for example because the target cell is marked invalid or blocked by `eventOverlap`.
- Build the add/edit popup with the `Popup` component. Use the `responsive` option to switch from a full-screen bottom sheet on mobile to a centered fixed-width dialog on larger screens.
- Render a custom popup header with a primary line - such as "Edit Ryan's hours" or "New shift" - and a secondary line with the day name, slot name, and date.
- Inside the popup, use a `Datepicker` in range mode with `controls={['time']}`. Connect it to two `Input` fields via `startInput` and `endInput` refs so the start and end times appear as separate labelled inputs that open the shared range picker. Constrain the allowed range with `minTime` and `maxTime` derived from the current slot.
- For deletion, add a Delete button to the edit popup that removes the shift immediately and closes the popup, then shows a `Snackbar` with an Undo action that restores the deleted shift if the user reverses the action.

## 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-javascript.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 javascript
   ```

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

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

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

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: [Work Order Scheduling](https://demo.mobiscroll.com/javascript/timeline/work-order-scheduling.md) - Schedule work orders across grouped resources — drivers, trucks, and contractors — on a Javascript timeline.
