Shifts and Tasks
Usage
theme="ios" You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant="light" You can turn auto dark mode on based on system settings.
Usage
clickToCreate={true}
// click to create is on dragToCreate={true}
// drag to create is on dragToMove={true}
// reordering is on dragToResize={true}
// resizing is on eventDelete={true}
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale={localeEn} Selecting the locale loads the localized settings and the translated UI text.
Shifts and Tasks
Visualize shifts and tasks within them using the timeline view. With the help of the order 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 through the onEventCreated, onEventUpdated and onEventDeleted lifecycle events.
Demo

Code
View code on GitHub
import { Eventcalendar, setOptions, Toast } from '@mobiscroll/react';
import { useCallback, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const blockedOutTimes = [
{
start: '00:00',
end: '05:00',
recurring: {
repeat: 'daily',
},
workOff: true,
},
{
start: '19:00',
end: '00:00',
recurring: {
repeat: 'daily',
},
workOff: true,
},
];
function App() {
const timelineInst = useRef();
const [myInvalids, setInvalid] = useState(blockedOutTimes);
const [isToastOpen, setToastOpen] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const myResources = useMemo(
() => [
{
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,
},
],
[],
);
const [myEvents, setEvents] = useState([
{
id: 1,
start: '2026-10-24T05:00',
end: '2026-10-24T12: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-24T05:00',
end: '2026-10-24T08:30',
title: 'Server Maintenance',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'es-2',
start: '2026-10-24T09:00',
end: '2026-10-24T10:45',
title: 'Monitor System Performance',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'es-3',
start: '2026-10-24T11:00',
end: '2026-10-24T12:00',
title: 'Backup and Recovery',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
]);
const myView = useMemo(
() => ({
timeline: {
type: 'week',
startDay: 1,
startTime: '05:00',
endTime: '19:00',
endDay: 5,
eventHeight: 'variable',
},
}),
[],
);
const myDefaultEvent = useCallback((args) => {
if (timelineInst.current) {
const events = timelineInst.current
.getEvents(args.start, new Date(+args.start + 3600000))
.filter((e) => e.resource === args.resource);
const 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,
};
}
}, []);
const handelEventCreate = useCallback(
(args, inst) => {
const event = args.event;
const overlapEvents = inst.getEvents(event.start, event.end).filter((e) => e.resource === event.resource);
if (event.shift) {
// Tasks was created
const shift = { ...overlapEvents[0] };
const shiftIndex = myEvents.findIndex((x) => x.id === event.shift);
const newEventList = [...myEvents];
if (overlapEvents.length > 2) {
// Prevent overlap
setEvents(newEventList);
setToastMessage('No space for task');
setToastOpen(true);
} else {
// Update the shift
shift.tasks.push(event.id);
newEventList.splice(shiftIndex, 1, 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;
newEventList.push(event);
setEvents(newEventList);
}
} else {
setEvents([...myEvents, event]);
}
},
[myEvents],
);
const handelEventDelete = useCallback(
(args) => {
let newEventList = [...myEvents];
const event = args.event;
const evIndex = myEvents.findIndex((x) => x.id === event.id);
newEventList.splice(evIndex, 1);
if (event.tasks) {
// Update shift tasks
newEventList = newEventList.filter((ev) => !event.tasks.includes(ev.id));
}
if (event.shift) {
const shift = newEventList.find((ev) => ev.resource === event.resource && ev.id === event.shift);
// Remove the deleted task id from the shift data
shift.tasks = shift.tasks.filter((t) => t !== event.id);
}
setEvents(newEventList);
},
[myEvents],
);
const handelEventDragStart = useCallback((args, inst) => {
const events = inst.getEvents();
const event = args.event;
const tempInvalid = [];
if (event.tasks) {
// Shift
const shiftsInResource = events.filter((e) => e.tasks !== undefined && e.resource === event.resource && e.id !== event.id);
shiftsInResource.forEach((e) => {
tempInvalid.push({
start: e.start,
end: e.end,
resource: e.resource,
cssClass: 'mds-task-blocked',
shift: true,
});
});
setInvalid([...blockedOutTimes, ...tempInvalid]);
} else {
// Subtask
const shift = events.find((ev) => 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,
},
);
setInvalid([...blockedOutTimes, ...tempInvalid]);
}
}, []);
const handelEventDragEnd = useCallback(() => {
setInvalid(blockedOutTimes);
}, []);
const handelEventUpdate = useCallback(
(args, inst) => {
const newEventList = [...myEvents];
const event = args.event;
const evIndex = newEventList.findIndex((x) => x.id === event.id);
const oldEvent = args.oldEvent;
if (event.tasks) {
// Shift was updated
let shiftStart = new Date(event.start);
let shiftEnd = new Date(event.end);
let subTasksDuration = 0;
const startDiff = +shiftStart - +new Date(oldEvent.start);
const endDiff = +shiftEnd - +new Date(oldEvent.end);
const startResize = startDiff > 0;
const endResize = endDiff < 0;
const isMove = startDiff === endDiff;
const isResize = !isMove && (startResize || endResize);
const tasks = event.tasks.map((el) => {
const t = newEventList.find((e) => 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;
newEventList.splice(evIndex, 1, event);
setEvents(newEventList);
}
// Resize or move
if (isResize || startDiff === endDiff) {
// Update subtask
tasks.forEach((task, i) => {
const taskStart = new Date(task.start);
const 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);
}
const taskIndex = newEventList.findIndex((x) => x.id === task.id);
newEventList.splice(taskIndex, 1, task);
});
setEvents(newEventList);
}
} else {
// Subtask was updated
const eventOverlap = inst.getEvents(event.start, event.end).filter((e) => e.resource === event.resource);
if (eventOverlap.length > 2) {
// Don't let subtask to overlap
newEventList.splice(evIndex, 1, oldEvent);
setEvents(newEventList);
setToastMessage('No space for task');
setToastOpen(true);
}
}
},
[myEvents],
);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
const handelEventUpdateFailed = useCallback((args) => {
const invalid = args.invalid;
let 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';
}
if (msg) {
setToastMessage(msg);
setToastOpen(true);
}
}, []);
const myEvent = useCallback((args) => {
const duration = (+args.endDate - +args.startDate) / 3600000;
return (
<>
{args.title}
<span className="mds-task-hours"> - {duration + (duration === 1 ? 'h' : 'hrs')}</span>
</>
);
}, []);
return (
<>
<Eventcalendar
dragToCreate={true}
dragToMove={true}
dragToResize={true}
ref={timelineInst}
view={myView}
data={myEvents}
invalid={myInvalids}
extendDefaultEvent={myDefaultEvent}
onEventCreated={handelEventCreate}
onEventDeleted={handelEventDelete}
onEventUpdated={handelEventUpdate}
onEventDragStart={handelEventDragStart}
onEventDragEnd={handelEventDragEnd}
onEventUpdateFailed={handelEventUpdateFailed}
resources={myResources}
renderTimelineEventContent={myEvent}
/>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</>
);
}
export default App;
import {
Eventcalendar,
MbscCalendarEvent,
MbscCalendarEventData,
MbscEventcalendarView,
MbscEventCreatedEvent,
MbscEventDeletedEvent,
MbscEventDragEvent,
MbscEventUpdateEvent,
MbscEventUpdateFailedEvent,
MbscNewEventData,
MbscResource,
setOptions,
Toast ,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
interface MyEvent extends MbscCalendarEvent {
shift?: number | string;
tasks?: Array<string | number>;
}
interface MyBlockedTimes extends MbscCalendarEvent {
shift?: boolean;
task?: boolean;
workOff?: boolean;
}
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const blockedOutTimes: MyBlockedTimes[] = [
{
id: 1,
start: '00:00',
end: '05:00',
recurring: {
repeat: 'daily',
},
workOff: true,
},
{
id: 2,
start: '19:00',
end: '00:00',
recurring: {
repeat: 'daily',
},
workOff: true,
},
];
const App: FC = () => {
const timelineInst = useRef<Eventcalendar>(null);
const [myInvalids, setInvalid] = useState(blockedOutTimes);
const [isToastOpen, setToastOpen] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [myEvents, setEvents] = useState<MyEvent[]>([
{
id: 1,
start: '2026-10-24T05:00',
end: '2026-10-24T12: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-24T05:00',
end: '2026-10-24T08:30',
title: 'Server Maintenance',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'es-2',
start: '2026-10-24T09:00',
end: '2026-10-24T10:45',
title: 'Monitor System Performance',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'es-3',
start: '2026-10-24T11:00',
end: '2026-10-24T12:00',
title: 'Backup and Recovery',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
]);
const myResources = useMemo(
() => [
{
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,
},
],
[],
);
const myView = useMemo<MbscEventcalendarView>(
() => ({
timeline: {
type: 'week',
startDay: 1,
startTime: '05:00',
endTime: '19:00',
endDay: 5,
eventHeight: 'variable',
},
}),
[],
);
const myDefaultEvent = useCallback((args: MbscNewEventData): MbscCalendarEvent => {
const events = timelineInst.current
? timelineInst.current.getEvents(args.start, new Date(+args.start + 3600000)).filter((e) => e.resource === args.resource)
: [];
const 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,
};
}, []);
const handelEventCreate = useCallback(
(args: MbscEventCreatedEvent) => {
const inst = args.inst!;
const event = args.event;
const overlapEvents = inst.getEvents(event.start, event.end).filter((e) => e.resource === event.resource);
if (event.shift) {
// Tasks was created
const shift = { ...overlapEvents[0] };
const shiftIndex = myEvents.findIndex((x) => x.id === event.shift);
const newEventList = [...myEvents];
if (overlapEvents.length > 2) {
// Prevent overlap
setEvents(newEventList);
setToastMessage('No space for task');
setToastOpen(true);
} else {
// Update the shift
shift.tasks.push(event.id);
newEventList.splice(shiftIndex, 1, shift);
// Update subtask
event.start = new Date(Math.max(+new Date(shift.start as Date), +event.start!));
event.end = new Date(Math.min(+new Date(shift.end as Date), +event.end!));
event.shift = shift.id;
newEventList.push(event);
setEvents(newEventList);
}
} else {
setEvents([...myEvents, event]);
}
},
[myEvents],
);
const handelEventDelete = useCallback(
(args: MbscEventDeletedEvent) => {
let newEventList = [...myEvents];
const event = args.event;
const evIndex = myEvents.findIndex((x) => x.id === event.id);
newEventList.splice(evIndex, 1);
if (event.tasks) {
// Update shift tasks
newEventList = newEventList.filter((ev) => !event.tasks.includes(ev.id));
}
if (event.shift) {
const shift: MyEvent = newEventList.find((ev) => ev.resource === event.resource && ev.id === event.shift)!;
// Remove the deleted task id from the shift data
shift.tasks = shift.tasks && shift.tasks.filter((t) => t !== event.id);
}
setEvents(newEventList);
},
[myEvents],
);
const handelEventDragStart = useCallback((args: MbscEventDragEvent) => {
const inst = args.inst!;
const events = inst.getEvents();
const event = args.event;
const tempInvalid: MbscResource[] = [];
if (event.tasks) {
// Shift
const shiftsInResource = events.filter((e) => e.tasks !== undefined && e.resource === event.resource && e.id !== event.id);
shiftsInResource.forEach((e) => {
tempInvalid.push({
id: 'mds-s-' + e.id,
start: e.start,
end: e.end,
resource: e.resource,
cssClass: 'mds-task-blocked',
shift: true,
});
});
setInvalid([...blockedOutTimes, ...tempInvalid]);
} else {
// Subtask
const shift = events.find((ev) => ev.resource === event.resource && ev.id === event.shift)!;
tempInvalid.push(
{
id: 'mds-te-' + shift.id,
start: new Date(+new Date(shift.start as Date) - 7 * 86400000),
end: shift.start,
resource: shift.resource,
cssClass: 'mds-task-blocked',
task: true,
},
{
id: 'mds-tu-' + shift.id,
start: shift.end,
end: new Date(+new Date(shift.end as Date) + 7 * 86400000),
resource: shift.resource,
cssClass: 'mds-task-blocked',
task: true,
},
);
setInvalid([...blockedOutTimes, ...tempInvalid]);
}
}, []);
const handelEventDragEnd = useCallback(() => {
setInvalid(blockedOutTimes);
}, []);
const handelEventUpdate = useCallback(
(args: MbscEventUpdateEvent) => {
const inst = args.inst!;
const newEventList = [...myEvents];
const event: MyEvent = args.event;
const evIndex = newEventList.findIndex((x) => x.id === event.id);
const oldEvent = args.oldEvent as MyEvent;
if (event.tasks) {
// Shift was updated
let shiftStart = new Date(event.start as Date);
let shiftEnd = new Date(event.end as Date);
let subTasksDuration = 0;
const startDiff = +shiftStart - +new Date(oldEvent!.start as Date);
const endDiff = +shiftEnd - +new Date(oldEvent!.end as Date);
const startResize = startDiff > 0;
const endResize = endDiff < 0;
const isMove = startDiff === endDiff;
const isResize = !isMove && (startResize || endResize);
const tasks = event.tasks.map((el) => {
const t: MyEvent = newEventList.find((e) => e.id === el)!;
subTasksDuration += +new Date(t!.end as Date) - +new Date(t!.start as Date);
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;
newEventList.splice(evIndex, 1, event);
setEvents(newEventList);
}
// Resize or move
if (isResize || startDiff === endDiff) {
// Update subtask
tasks.forEach((task: MyEvent, i) => {
const taskStart = new Date(task!.start as Date);
const taskEnd = new Date(task!.end as Date);
if (isResize) {
task.start = new Date(i === 0 ? +shiftStart : +new Date(tasks[i - 1].end as Date));
task.end = new Date(Math.min(+task.start + (+taskEnd - +taskStart), +new Date(event.end as Date)));
} else {
task.start = new Date(+taskStart + startDiff);
task.end = new Date(+taskEnd + startDiff);
}
const taskIndex = newEventList.findIndex((x) => x.id === task.id);
newEventList.splice(taskIndex, 1, task);
});
setEvents(newEventList);
}
} else {
// Subtask was updated
const eventOverlap = inst.getEvents(event.start, event.end).filter((e) => e.resource === event.resource);
if (eventOverlap.length > 2) {
// Don't let subtask to overlap
newEventList.splice(evIndex, 1, oldEvent);
setEvents(newEventList);
setToastMessage('No space for task');
setToastOpen(true);
}
}
},
[myEvents],
);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
const handelEventUpdateFailed = useCallback((args: MbscEventUpdateFailedEvent) => {
const invalid = args.invalid;
let 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';
}
if (msg) {
setToastMessage(msg);
setToastOpen(true);
}
}, []);
const myEvent = useCallback((args: MbscCalendarEventData) => {
const duration = (+args.endDate - +args.startDate) / 3600000;
return (
<>
{args.title}
<span className="mds-task-hours"> - {duration + (duration === 1 ? 'h' : 'hrs')}</span>
</>
);
}, []);
return (
<>
<Eventcalendar
ref={timelineInst}
dragToCreate={true}
dragToMove={true}
dragToResize={true}
view={myView}
data={myEvents}
invalid={myInvalids}
extendDefaultEvent={myDefaultEvent}
onEventCreated={handelEventCreate}
onEventDeleted={handelEventDelete}
onEventUpdated={handelEventUpdate}
onEventDragStart={handelEventDragStart}
onEventDragEnd={handelEventDragEnd}
onEventUpdateFailed={handelEventUpdateFailed}
resources={myResources}
renderTimelineEventContent={myEvent}
/>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</>
);
};
export default App;
.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);
}