---
title: "Javascript Timeline Shifts and Tasks Example | Mobiscroll"
component: "Timeline"
framework: "JavaScript"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/javascript/timeline/tasks-subtasks-under-shifts"
---

# Shifts and Tasks - JavaScript Timeline demo

Display shifts and linked tasks on a Javascript Timeline — group tasks under parent shifts and restrict movement to each shift's time range. Plain JavaScript API.

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

## Description

Visualize shifts and tasks within them using the timeline view. With the help of the [`order`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-data) property of the data object you can always guarantee that shifts are always displayed at the top (alternatively below tasks) and tasks within those shifts are rendered below.

The shift and task events are bind together with custom data properties. Every shift contains the task IDs in the `tasks` property, and every task has a `shift` property which indicates the parent shift. These properties are used later in the validation logic to handle the two distinct event type behaviours. The validation is implemented by dynamically setting [invalid ranges](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-invalid) through the [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated), [`onEventUpdated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdated) and [`onEventDeleted`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDeleted) lifecycle events.

**Do you want to learn about the event ordering?** [Learn more about it in the documentation →](https://mobiscroll.com/docs/javascript/eventcalendar/timeline#event-order)

## What this demo shows

- A desktop weekly timeline for planning shifts and tasks together, with Monday to Friday workdays arranged horizontally and employee resources listed vertically on the left.
- **Header navigation** The month and year label opens date navigation, while the previous and next arrows and the Today button move between weeks or return to the current day.
- **Week view** The fixed date strip shows the selected work week from Monday to Friday using the `DD DDD MMM YYYY` date format, with the current date highlighted.
- **Time grid** The timeline displays working hours from 5 AM to 7 PM with hourly columns under each day.
- **Resources** The left side lists five employee resources.
- **Current time** A vertical blue line and time label mark the current time.
- **Hover behavior** Hovering over the time grid shows a time indicator that follows the cursor in 15-minute increments.
- **Shift labels** Shifts are rendered as gray labels above the task events and can span an employee's working hours, including shift types such as Daily Shift and Flex Shift.
- **Shift constraints** Shift labels can be moved in time, but they cannot be moved between resources or overlap with other shifts.
- **Grouped tasks** Task events are grouped under their parent shifts. When a shift is updated, the tasks assigned to that shift move with it.
- **Task constraints** Task events can be moved in time only within their parent shift period.
- **Event cards** Task events are rendered as colored cards with a colored stripe on the left, a bold task name, a calculated duration next to the title, and the exact start and end time below the title.
- **Event creation** New events can be created by double-clicking an individual resource row or by clicking and dragging across a time range.
- **Shift-first creation** Empty time ranges create a shift first; tasks can then be created and assigned to that shift.
- **Event interaction** Events are highlighted on hover and show drag and resize handles for repositioning or changing duration.
- **Event selection** Clicking an event selects and highlights it.
- **Horizontal scrolling** Horizontal scrolling moves across multiple days while keeping employee rows visible.

## Best for

- **Workforce scheduling** Planning employee shifts and the tasks assigned within each shift.
- **Shift management** Coordinating daily and flexible shifts while keeping work items tied to the correct employee and time range.
- **Service operations** Scheduling field, support, or operations work where shift coverage and task timing need to be managed together.
- **IT support planning** Assigning support tasks within employee availability windows.
- **Maintenance teams** Organizing maintenance work by employee, workday, shift, and task duration.
- **Employee workload management** Reviewing how individual work items fit into each employee's scheduled working hours.

## 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
- [`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
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-extendDefaultEvent) - option
- [`getEvents`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-getEvents) - method
- [`cssClass`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-cssClass) - option
- [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated) - event
- [`removeEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-removeEvent) - method
- [`updateEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-updateEvent) - method
- [`onEventDeleted`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDeleted) - event
- [`onEventDragStart`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDragStart) - event
- [`setOptions`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-setOptions) - method
- [`invalid`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-invalid) - option
- [`onEventDragEnd`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDragEnd) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdated) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdateFailed) - event
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`data`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-resources) - option

## Code

### JS

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

var myInvalids = [
  {
    start: '00:00',
    end: '05:00',
    recurring: {
      repeat: 'daily',
    },
    workOff: true,
  },
  {
    start: '19:00',
    end: '00:00',
    recurring: {
      repeat: 'daily',
    },
    workOff: true,
  },
];

var timelineInst = mobiscroll.eventcalendar('#demo-tasks-subtasks-under-shifts', {
  dragToCreate: true,
  dragToMove: true,
  dragToResize: true,
  view: {
    timeline: {
      type: 'week',
      startDay: 1,
      startTime: '05:00',
      endTime: '19:00',
      endDay: 5,
      eventHeight: 'variable',
    },
  },
  extendDefaultEvent: function (args) {
    var events = timelineInst.getEvents(args.start, new Date(+args.start + 3600000)).filter(function (e) {
      return e.resource === args.resource;
    });
    var isShift = events.length === 0;
    return {
      order: isShift ? 1 : 2,
      cssClass: isShift ? 'mds-task-shift' : 'mds-task-subtask',
      color: isShift ? '#513737' : '',
      title: isShift ? 'New Shift' : 'New Task',
      tasks: isShift ? [] : undefined,
      shift: isShift ? undefined : events[0].id,
    };
  },
  onEventCreated: function (args, inst) {
    var event = args.event;
    var overlapEvents = inst.getEvents(event.start, event.end).filter(function (e) {
      return e.resource === event.resource;
    });

    if (event.shift) {
      // Task was created
      var shift = overlapEvents[0];
      if (overlapEvents.length > 2) {
        // Prevent task overlap
        inst.removeEvent(event);
        mobiscroll.toast({
          message: 'No space for task',
        });
      } else {
        // Update the shift
        shift.tasks.push(event.id);
        inst.updateEvent(shift);
        // Update subtask
        event.start = new Date(Math.max(+new Date(shift.start), +event.start));
        event.end = new Date(Math.min(+new Date(shift.end), +event.end));
        event.shift = shift.id;
        inst.updateEvent(event);
      }
    }
  },
  onEventDeleted: function (args, inst) {
    var event = args.event;
    if (event.tasks) {
      inst.removeEvent(event.tasks);
    }

    if (event.shift) {
      var shift = inst.getEvents().find(function (ev) {
        return ev.resource === event.resource && ev.id === event.shift;
      });
      // Remove the deleted task id from the shift data
      shift.tasks = shift.tasks.filter(function (t) {
        return t !== event.id;
      });

      inst.updateEvent(shift);
    }
  },
  onEventDragStart: function (args, inst) {
    var events = inst.getEvents();
    var event = args.event;
    var tempInvalid = [];

    if (event.tasks) {
      // Shift
      var shiftsInResource = events.filter(function (e) {
        return e.tasks !== undefined && e.resource === event.resource && e.id !== event.id;
      });

      shiftsInResource.forEach(function (e) {
        tempInvalid.push({
          start: e.start,
          end: e.end,
          resource: e.resource,
          cssClass: 'mds-task-blocked',
          shift: true,
        });
      });

      inst.setOptions({
        invalid: tempInvalid.concat(myInvalids),
      });
    } else {
      // Subtask
      var shift = events.find(function (ev) {
        return ev.resource === event.resource && ev.id === event.shift;
      });
      tempInvalid.push(
        {
          start: new Date(+new Date(shift.start) - 7 * 86400000),
          end: shift.start,
          resource: shift.resource,
          cssClass: 'mds-task-blocked',
          task: true,
        },
        {
          start: shift.end,
          end: new Date(+new Date(shift.end) + 7 * 86400000),
          resource: shift.resource,
          cssClass: 'mds-task-blocked',
          task: true,
        },
      );
      inst.setOptions({
        invalid: tempInvalid.concat(myInvalids),
      });
    }
  },
  onEventDragEnd: function (args, inst) {
    inst.setOptions({
      invalid: myInvalids,
    });
  },
  onEventUpdated: function (args, inst) {
    var events = inst.getEvents();
    var event = args.event;
    var oldEvent = args.oldEvent;

    if (event.tasks) {
      // Shift was updated
      var shiftStart = new Date(event.start);
      var shiftEnd = new Date(event.end);
      var startDiff = +shiftStart - +new Date(oldEvent.start);
      var endDiff = +shiftEnd - +new Date(oldEvent.end);
      var startResize = startDiff > 0;
      var endResize = endDiff < 0;
      var isMove = startDiff === endDiff;
      var isResize = !isMove && (startResize || endResize);
      var subTasksDuration = 0;
      var tasks = event.tasks.map(function (el) {
        var t = events.find(function (e) {
          return e.id === el;
        });
        subTasksDuration += +new Date(t.end) - +new Date(t.start);
        return t;
      });

      if (isResize && +shiftEnd - +shiftStart < subTasksDuration) {
        // Limit the shift to don't be smaller than the containing subtasks
        shiftStart = endResize ? shiftStart : new Date(+shiftEnd - subTasksDuration);
        shiftEnd = startResize ? shiftEnd : new Date(+shiftStart + subTasksDuration);
        event.start = shiftStart;
        event.end = shiftEnd;
        inst.updateEvent(event);
      }

      // Resize or move
      if (isResize || startDiff === endDiff) {
        var updatedTasks = [];
        // Update subtask
        tasks.forEach(function (task, i) {
          var taskStart = new Date(task.start);
          var taskEnd = new Date(task.end);
          if (isResize) {
            task.start = new Date(i === 0 ? +shiftStart : +new Date(tasks[i - 1].end));
            task.end = new Date(Math.min(+task.start + (+taskEnd - +taskStart), +new Date(event.end)));
          } else {
            task.start = new Date(+taskStart + startDiff);
            task.end = new Date(+taskEnd + startDiff);
          }
          updatedTasks.push(task);
        });

        inst.updateEvent(updatedTasks);
      }
    } else {
      // Subtask was updated
      var eventOverlap = inst.getEvents(event.start, event.end).filter(function (e) {
        return e.resource === event.resource;
      });
      if (eventOverlap.length > 2) {
        // Don't let subtask to overlap
        inst.updateEvent(oldEvent);

        mobiscroll.toast({
          message: 'No space for task',
        });
      }
    }
  },
  onEventUpdateFailed: function (args) {
    var invalid = args.invalid;
    var msg = '';

    if (invalid.workOff) {
      msg = 'Shift falls out of working hours';
    } else if (invalid.task) {
      msg = 'Task falls out of shift';
    } else if (invalid.shift) {
      msg = 'Shifts cannot overlap';
    }

    msg &&
      mobiscroll.toast({
        message: msg,
      });
  },
  renderTimelineEventContent: function (args) {
    var duration = (+args.endDate - +args.startDate) / 3600000;
    return (
      args.title +
      (args.original.shift ? ' <span class="mds-task-hours"> - ' + (duration + (duration === 1 ? 'h' : 'hrs')) + '</span>' : '')
    );
  },
  data: [
    {
      id: 1,
      start: '2026-10-25T05:00',
      end: '2026-10-25T12:00',
      title: 'Daily Shift',
      resource: 1,
      tasks: ['es-1', 'es-2', 'es-3'],
      order: 1,
      color: '#513737',
      cssClass: 'mds-task-shift',
    },
    {
      id: 'es-1',
      start: '2026-10-25T05:00',
      end: '2026-10-25T08:30',
      title: 'Server Maintenance',
      resource: 1,
      shift: 1,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
    {
      id: 'es-2',
      start: '2026-10-25T09:00',
      end: '2026-10-25T10:45',
      title: 'Monitor System Performance',
      resource: 1,
      shift: 1,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
    {
      id: 'es-3',
      start: '2026-10-25T11:00',
      end: '2026-10-25T12:00',
      title: 'Backup and Recovery',
      resource: 1,
      shift: 1,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
    {
      id: 2,
      start: '2026-10-25T11:00',
      end: '2026-10-25T18:00',
      title: 'Flex Shift',
      resource: 2,
      tasks: ['jb-1', 'jb-2', 'jb-3', 'jb-4'],
      order: 1,
      color: '#513737',
      cssClass: 'mds-task-shift',
    },
    {
      id: 'jb-1',
      start: '2026-10-25T11:00',
      end: '2026-10-25T12:30',
      title: 'Code Review',
      resource: 2,
      shift: 2,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
    {
      id: 'jb-2',
      start: '2026-10-25T13:00',
      end: '2026-10-25T14:45',
      title: 'Develop New Features',
      resource: 2,
      shift: 2,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
    {
      id: 'jb-3',
      start: '2026-10-25T15:00',
      end: '2026-10-25T16:50',
      title: 'Mentor Junior Developers',
      resource: 2,
      shift: 2,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
    {
      id: 'jb-4',
      start: '2026-10-25T17:00',
      end: '2026-10-25T18:00',
      title: 'Attend Stand-Up Meeting',
      resource: 2,
      shift: 2,
      order: 2,
      cssClass: 'mds-task-subtask',
    },
  ],
  resources: [
    {
      id: 1,
      color: '#ff0101',
      name: 'Emma Smith',
      eventDragBetweenResources: false,
    },
    {
      id: 2,
      color: '#239a21',
      name: 'James Brown',
      eventDragBetweenResources: false,
    },
    {
      id: 3,
      color: '#8f1ed6',
      name: 'Olivia Miller',
      eventDragBetweenResources: false,
    },
    {
      id: 4,
      color: '#01adff',
      name: 'Robert Taylor',
      eventDragBetweenResources: false,
    },

    {
      id: 5,
      color: '#F58585',
      name: 'John Doe',
      eventDragBetweenResources: false,
    },
  ],
  invalid: myInvalids,
});
```

### HTML

```html
<div id="demo-tasks-subtasks-under-shifts"></div>
```

### CSS

```css
.mds-task-shift {
  height: 30px;
}

.mds-task-subtask {
  height: 40px;
}

.mds-task-subtask .mbsc-schedule-event-range,
.mds-task-subtask .mbsc-schedule-event-title  {
  font-size: 10px;
  line-height: 13px;
}

.mds-task-shift .mbsc-schedule-event-range,
.mds-task-shift .mbsc-schedule-event-title {
  display: inline-block;
  margin: 0 4px;
}

.mds-task-hours {
  font-size: 10px;
  font-weight: 400;
}

.mds-task-blocked.mbsc-schedule-invalid {
  text-align: center;
  align-items: center;
  font-weight: bold;
  background: repeating-linear-gradient(-45deg, #f3f3f3, #f3f3f3 11px, #e5e5e5 11px, #e5e5e5 22px);
}
```

## Implementation notes

- Use `timeline: { type: 'week', startDay: 1, endDay: 5, startTime: '05:00', endTime: '19:00', eventHeight: 'variable' }` - a Mon-Fri week view with a variable row height so shifts and tasks can stack at different heights in the same resource row.
- Define 5 resources: Emma Smith (`#ff0101`), James Brown (`#239a21`), Olivia Miller (`#8f1ed6`), Robert Taylor (`#01adff`), John Doe (`#F58585`). Set `eventDragBetweenResources: false` on every resource - tasks and shifts must stay on their assigned person.
- **Shift/task data model** - every event carries an `order` property to control stacking:
  - **Shifts**: `order: 1`, `cssClass: 'mds-task-shift'`, `color: '#513737'`, and a `tasks` array listing the IDs of their child subtasks (e.g. `tasks: ['es-1', 'es-2', 'es-3']`).
  - **Subtasks**: `order: 2`, `cssClass: 'mds-task-subtask'`, and a `shift` property holding the parent shift's ID (e.g. `shift: 1`). Lower `order` renders first (top), higher `order` renders below - shifts always appear above their tasks.
- **Static invalids** - block 00:00-05:00 and 19:00-00:00 daily with `workOff: true` on each entry, forming the base `myInvalids` list that is always active.
- Enable `dragToCreate: true`, `dragToMove: true`, `dragToResize: true`.
- **`extendDefaultEvent`** - determines whether a new drag-create produces a shift or a subtask. Use `inst.getEvents(args.start, args.start + 1h)` filtered by `args.resource`: if no event overlaps, the slot is empty → create a shift (`order: 1`, `tasks: []`); otherwise create a subtask (`order: 2`, `shift: overlappingEvent.id`).
- **`onEventCreated`** - if the new event has a `shift` property (it is a task):
  - If `overlapEvents.length > 2` (shift + existing task + new task), remove the new event and show a toast: "No space for task".
  - Otherwise: push the new task's ID into `shift.tasks`, clamp the task's start/end within the shift bounds, and persist both updates.
- **`onEventDeleted`** - if the deleted event is a shift, also remove all its child task IDs from the event list. If it is a subtask, remove its ID from the parent shift's `tasks` array.
- **`onEventDragStart`** - build temporary invalid ranges to visually block illegal drop zones during drag:
  - Dragging a **shift**: mark every other shift on the same resource as blocked (class `mds-task-blocked`, custom `shift: true` flag).
  - Dragging a **subtask**: block the 7-day windows before `shift.start` and after `shift.end` on the same resource (class `mds-task-blocked`, custom `task: true` flag).
  - Merge with `blockedOutTimes` and set as the current `invalid` list.
- **`onEventDragEnd`** - reset `invalid` back to `blockedOutTimes` only.
- **`onEventUpdated`** - if the updated event is a **shift**: compute `startDiff` / `endDiff` to detect move vs. resize; propagate the same offset to all child tasks. If the resize would make the shift smaller than the combined subtask durations, clamp the shift edge. If the updated event is a **subtask**: reject the update (revert to `oldEvent`) and show a toast if it would overlap another event on the same resource.
- **`onEventUpdateFailed`** - inspect `args.invalid` for the three custom flags to show a contextual toast:
  - `workOff` → "Shift falls out of working hours"
  - `task` → "Task falls out of shift"
  - `shift` → "Shifts cannot overlap"
- **`renderTimelineEventContent`** - render the event title plus a duration badge for subtasks: `"<title> - <N>h"` / `"<title> - <N>hrs"`. Subtask events have a smaller, lighter-weight duration label rendered after the title.
- Angular: define a `MyEvent` interface extending `MbscCalendarEvent` with `shift?: number | string` and `tasks?: (string | number)[]`. Inject `Notifications` for toasts. Use `@ViewChild('timeline') timelineInst` to access the instance. Pass event-handler methods in `calendarOptions` instead of inline template bindings.
- Vue: use `MbscToast` component with `isOpen` / `message` reactive refs for toast feedback.
- JavaScript/jQuery: use `inst.removeEvent()` and `inst.updateEvent()` for imperative event mutations; use `inst.setOptions({ invalid: ... })` to swap the invalid list on drag.

## 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

- Previous: [Task Progress Tracking](https://demo.mobiscroll.com/javascript/timeline/show-task-progress-on-event.md) - Show task completion progress on Javascript Timeline events with an inline hover slider for live updates and a progress field in the add/edit dialog.
- Next: [Resource Search and Filtering](https://demo.mobiscroll.com/javascript/timeline/resource-filtering-search.md) - Add live search and filter to a Javascript Timeline resource header — multi-select by status or group, with a custom empty state and reset.
