Task Progress Tracking
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.
Task Progress Tracking
Showing percentage progress, or even having a visual overlay that indicates the progress of a task is sometimes necessary, not just for full fledged Gantt charts. Although being usually associated with Gantt charts, having a clear overview of progress with tools that enable live update is useful in a lot of situations.
Using the event templating capabilities, it is possible to render a live slider that reacts on hover and enables updating the progress inline. Use the renderTimelineEvent function to customize the template and adjust it to your needs.
Besides being able to update progress right on the event, it is also possible to add a slider that controls task progression - saved in a custom property of the event - inside an add/edit dialog.
Demo

Code
View code on GitHub
import {
Datepicker,
Eventcalendar,
Input,
Popup,
setOptions ,
} from '@mobiscroll/react';
import { useCallback, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const defaultEvents = [
{
start: '2026-10-23T00:00',
end: '2026-10-26T00:00',
title: 'Design Homepage',
resource: 'alice',
progress: 100,
},
{
start: '2026-10-23T00:00',
end: '2026-10-27T00:00',
title: 'Create Wireframes',
resource: 'bob',
progress: 100,
},
{
start: '2026-10-25T00:00',
end: '2026-10-30T00:00',
title: 'Develop Frontend',
resource: 'charlie',
progress: 45,
},
{
start: '2026-10-25T00:00',
end: '2026-10-30T00:00',
title: 'Develop Backend',
resource: 'dave',
progress: 35,
},
{
start: '2026-10-30T00:00',
end: '2026-11-03T00:00',
title: 'Test Website',
resource: 'erin',
progress: 0,
},
{
start: '2026-10-27T00:00',
end: '2026-11-03T00:00',
title: 'Fix Bugs',
resource: 'frank',
progress: 0,
},
{
start: '2026-11-03T00:00',
end: '2026-11-06T00:00',
title: 'Deploy Website',
resource: 'george',
progress: 0,
},
];
const myResources = [
{
id: 'gro1',
name: 'Designer Team',
color: '#76e083',
eventCreation: false,
children: [
{
id: 'alice',
name: 'Alice',
title: 'Designer',
color: '#1dab2f',
},
{
id: 'bob',
name: 'Bob',
title: 'Designer',
color: '#76e083',
},
],
},
{
id: 'gro2',
name: 'Development Team',
color: '#ff1717',
eventCreation: false,
children: [
{
id: 'charlie',
name: 'Charlie',
title: 'Frontend Developer',
color: '#4981d6',
},
{
id: 'dave',
name: 'Dave',
title: 'Backend Developer',
color: '#f7961e',
},
{
id: 'frank',
name: 'Frank',
title: 'Full-Stack Developer',
color: '#34c8e0',
},
{
id: 'george',
name: 'George',
title: 'DevOps Engineer',
color: '#e25dd2',
},
],
},
{
id: 'gro3',
name: 'QA Team',
color: '#d6d145',
eventCreation: false,
children: [
{
id: 'erin',
name: 'Erin',
title: 'QA Tester',
color: '#d6d145',
},
],
},
];
function App() {
const [myEvents, setMyEvents] = useState(defaultEvents);
const [tempEvent, setTempEvent] = useState(null);
const [isPopupOpen, setPopupOpen] = useState(false);
const [isEdit, setEdit] = useState(false);
const [startInput, startInputRef] = useState(null);
const [endInput, endInputRef] = useState(null);
const [popupAnchor, setPopupAnchor] = useState(null);
const [popupEventTitle, setTitle] = useState('');
const [popupEventDate, setDate] = useState([]);
const [popupEventResource, setResource] = useState('');
const [popupEventProgress, setProgress] = useState(0);
const isDraggingProgress = useRef(false);
const myView = useMemo(() => ({ timeline: { type: 'month', eventDisplay: 'fill' } }), []);
const loadPopupForm = useCallback((event) => {
setTitle(event.title);
setDate([event.start, event.end]);
setResource(event.resource);
setProgress(event.progress || 0);
}, []);
const updateEvent = useCallback(
(updatedEvent) => {
// Update the event in the list
const index = myEvents.findIndex((event) => event.id === updatedEvent.id);
const newEventList = [...myEvents];
newEventList.splice(index, 1, updatedEvent);
setMyEvents(newEventList);
},
[myEvents],
);
const saveEvent = useCallback(() => {
const newEvent = {
id: tempEvent.id,
title: popupEventTitle,
start: popupEventDate[0],
end: popupEventDate[1],
resource: popupEventResource,
progress: popupEventProgress,
};
if (isEdit) {
updateEvent(newEvent);
} else {
// Add the new event to the list
setMyEvents([...myEvents, newEvent]);
}
setPopupOpen(false);
}, [isEdit, myEvents, popupEventDate, popupEventTitle, popupEventProgress, popupEventResource, tempEvent, updateEvent]);
const popupHeaderText = useMemo(() => (isEdit ? 'Edit event' : 'New Event'), [isEdit]);
const popupButtons = useMemo(
() =>
isEdit
? [
'cancel',
{
handler: saveEvent,
keyCode: 'enter',
text: 'Save',
cssClass: 'mbsc-popup-button-primary',
},
]
: [
'cancel',
{
handler: saveEvent,
keyCode: 'enter',
text: 'Add',
cssClass: 'mbsc-popup-button-primary',
},
],
[isEdit, saveEvent],
);
const popupResponsive = useMemo(
() => ({
medium: {
display: 'anchored',
width: 400,
fullScreen: false,
touchUi: false,
},
}),
[],
);
const datepickerResponsive = useMemo(
() => ({
medium: {
touchUi: false,
},
}),
[],
);
const handleEventCreated = useCallback(
(args) => {
setEdit(false);
setTempEvent(args.event);
loadPopupForm(args.event);
setPopupAnchor(args.target);
setPopupOpen(true);
},
[loadPopupForm],
);
const handleEventClick = useCallback(
(args) => {
if (isDraggingProgress.current) {
return;
}
setEdit(true);
setTempEvent({ ...args.event });
loadPopupForm(args.event);
setPopupAnchor(args.domEvent.target);
setPopupOpen(true);
},
[loadPopupForm, isDraggingProgress],
);
const handleTitleChange = useCallback((ev) => {
setTitle(ev.target.value);
}, []);
const handleDateChange = useCallback((args) => {
setDate(args.value);
}, []);
const handleProgressChange = useCallback((ev) => {
setProgress(ev.target.value);
}, []);
const handlePopupClose = useCallback(() => {
if (!isEdit) {
// Refresh the list, if add popup was canceled, to remove the temporary event
setMyEvents([...myEvents]);
}
setPopupOpen(false);
}, [isEdit, myEvents]);
const handleEventUpdated = useCallback(
(args) => {
updateEvent(args.event);
},
[updateEvent],
);
const handleProgressArrowMouseDown = useCallback(
(e) => {
const progressArrow = e.target.closest('.mds-progress-arrow');
if (!progressArrow) {
return;
}
e.stopPropagation();
isDraggingProgress.current = true;
const progressBar = progressArrow.closest('.mds-progress-bar');
const progressLabel = progressArrow.closest('.mds-progress-event').querySelector('.mds-progress-label');
const eventContainerWidth = progressBar.parentElement.offsetWidth;
const initialMouseX = e.pageX;
const initialProgress = parseFloat(progressBar.style.width.replace('%', ''));
let newProgress;
const handleMouseMove = (e) => {
const mouseXOffset = e.pageX - initialMouseX;
newProgress = Math.round(initialProgress + (mouseXOffset / eventContainerWidth) * 100);
newProgress = Math.max(0, Math.min(100, newProgress));
progressBar.style.width = `${newProgress}%`;
progressLabel.textContent = `${newProgress}%`;
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
const eventId = progressArrow.dataset.eventId;
const eventToUpdate = myEvents.find((event) => event.id === eventId);
eventToUpdate.progress = newProgress;
updateEvent(eventToUpdate);
setTimeout(() => {
isDraggingProgress.current = false;
}, 100);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
},
[myEvents, updateEvent],
);
const renderCustomEvent = useCallback(
(event) => (
<div className="mds-progress-event" style={{ background: event.color }}>
<div className="mds-progress-bar" style={{ width: `${event.original.progress || 0}%` }}>
<div className="mds-progress-arrow" data-event-id={event.original.id}></div>
</div>
<div className="mds-progress-event-content">
<div className="mds-progress-event-title">{event.original.title}</div>
</div>
<div className="mds-progress-label" key={event.original.progress || 0}>
{event.original.progress || 0}%
</div>
</div>
),
[],
);
const renderCustomResource = useCallback(
(resource) => (
<div>
<div className="mds-progress-employee-name">{resource.name}</div>
{resource.title && <div className="mds-progress-employee-title">{resource.title}</div>}
</div>
),
[],
);
return (
<div onMouseDownCapture={handleProgressArrowMouseDown}>
<Eventcalendar
class="mds-progress-calendar"
view={myView}
data={myEvents}
resources={myResources}
clickToCreate={true}
dragToCreate={true}
dragToMove={true}
dragToResize={true}
onEventClick={handleEventClick}
onEventCreated={handleEventCreated}
onEventUpdated={handleEventUpdated}
renderResource={renderCustomResource}
renderTimelineEvent={renderCustomEvent}
/>
<Popup
display="bottom"
fullScreen={true}
contentPadding={false}
headerText={popupHeaderText}
anchor={popupAnchor}
buttons={popupButtons}
isOpen={isPopupOpen}
onClose={handlePopupClose}
responsive={popupResponsive}
>
<div className="mbsc-form-group">
<Input label="Title" value={popupEventTitle} onChange={handleTitleChange} />
</div>
<div className="mbsc-form-group">
<Input ref={startInputRef} label="Starts" />
<Input ref={endInputRef} label="Ends" />
<Datepicker
select="range"
touchUi={true}
startInput={startInput}
endInput={endInput}
showRangeLabels={false}
responsive={datepickerResponsive}
onChange={handleDateChange}
value={popupEventDate}
/>
</div>
<div className="mbsc-form-group">
<label className="mbsc-flex mbsc-align-items-center mbsc-padding">
<span>Progress</span>
<input
className="mds-popup-progress-slider mbsc-flex-1-0"
type="range"
min="0"
max="100"
value={popupEventProgress}
onChange={handleProgressChange}
/>
<span className="mds-popup-progress-label">{popupEventProgress}%</span>
</label>
</div>
</Popup>
</div>
);
}
export default App;
import {
Datepicker,
Eventcalendar,
Input,
MbscCalendarEvent,
MbscCalendarEventData,
MbscDatepickerChangeEvent,
MbscDateType,
MbscEventcalendarView,
MbscPopupButton,
MbscPopupOptions,
MbscResource,
MbscResponsiveOptions,
Popup,
setOptions ,
} from '@mobiscroll/react';
import { ChangeEvent, useCallback, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const defaultEvents: MbscCalendarEvent[] = [
{
start: '2026-10-23T00:00',
end: '2026-10-26T00:00',
title: 'Design Homepage',
resource: 'alice',
progress: 100,
},
{
start: '2026-10-23T00:00',
end: '2026-10-27T00:00',
title: 'Create Wireframes',
resource: 'bob',
progress: 100,
},
{
start: '2026-10-25T00:00',
end: '2026-10-30T00:00',
title: 'Develop Frontend',
resource: 'charlie',
progress: 45,
},
{
start: '2026-10-25T00:00',
end: '2026-10-30T00:00',
title: 'Develop Backend',
resource: 'dave',
progress: 35,
},
{
start: '2026-10-30T00:00',
end: '2026-11-03T00:00',
title: 'Test Website',
resource: 'erin',
progress: 0,
},
{
start: '2026-10-27T00:00',
end: '2026-11-03T00:00',
title: 'Fix Bugs',
resource: 'frank',
progress: 0,
},
{
start: '2026-11-03T00:00',
end: '2026-11-06T00:00',
title: 'Deploy Website',
resource: 'george',
progress: 0,
},
];
const myResources = [
{
id: 'gro1',
name: 'Designer Team',
color: '#76e083',
eventCreation: false,
children: [
{
id: 'alice',
name: 'Alice',
title: 'Designer',
color: '#1dab2f',
},
{
id: 'bob',
name: 'Bob',
title: 'Designer',
color: '#76e083',
},
],
},
{
id: 'gro2',
name: 'Development Team',
color: '#ff1717',
eventCreation: false,
children: [
{
id: 'charlie',
name: 'Charlie',
title: 'Frontend Developer',
color: '#4981d6',
},
{
id: 'dave',
name: 'Dave',
title: 'Backend Developer',
color: '#f7961e',
},
{
id: 'frank',
name: 'Frank',
title: 'Full-Stack Developer',
color: '#34c8e0',
},
{
id: 'george',
name: 'George',
title: 'DevOps Engineer',
color: '#e25dd2',
},
],
},
{
id: 'gro3',
name: 'QA Team',
color: '#d6d145',
eventCreation: false,
children: [
{
id: 'erin',
name: 'Erin',
title: 'QA Tester',
color: '#d6d145',
},
],
},
];
function App() {
const [myEvents, setMyEvents] = useState<MbscCalendarEvent[]>(defaultEvents);
const [tempEvent, setTempEvent] = useState<MbscCalendarEvent>();
const [isPopupOpen, setPopupOpen] = useState<boolean>(false);
const [isEdit, setEdit] = useState<boolean>(false);
const [startInput, startInputRef] = useState<Input | null>(null);
const [endInput, endInputRef] = useState<Input | null>(null);
const [popupAnchor, setPopupAnchor] = useState<HTMLElement>();
const [popupEventTitle, setTitle] = useState<string | undefined>('');
const [popupEventDate, setDate] = useState<MbscDateType[]>([new Date(), new Date()]);
const [popupEventResource, setResource] = useState<string>('');
const [popupEventProgress, setProgress] = useState<number>(0);
const isDraggingProgress = useRef(false);
const myView = useMemo<MbscEventcalendarView>(() => ({ timeline: { type: 'month', eventDisplay: 'fill' } }), []);
const loadPopupForm = useCallback((event: MbscCalendarEvent) => {
setTitle(event.title);
setDate([event.start!, event.end!]);
setResource(event.resource as string);
setProgress(event.progress || 0);
}, []);
const updateEvent = useCallback(
(updatedEvent: MbscCalendarEvent) => {
// Update the event in the list
const index = myEvents.findIndex((event) => event.id === updatedEvent.id);
const newEventList = [...myEvents];
newEventList.splice(index, 1, updatedEvent);
setMyEvents(newEventList);
},
[myEvents],
);
const saveEvent = useCallback(() => {
const newEvent = {
id: tempEvent!.id,
title: popupEventTitle,
start: popupEventDate[0],
end: popupEventDate[1],
resource: popupEventResource,
progress: popupEventProgress,
};
if (isEdit) {
updateEvent(newEvent);
} else {
// Add the new event to the list
setMyEvents([...myEvents, newEvent]);
}
setPopupOpen(false);
}, [isEdit, myEvents, popupEventDate, popupEventTitle, popupEventProgress, popupEventResource, tempEvent, updateEvent]);
const popupHeaderText = useMemo(() => (isEdit ? 'Edit event' : 'New Event'), [isEdit]);
const popupButtons = useMemo<(string | MbscPopupButton)[]>(
() =>
isEdit
? [
'cancel',
{
handler: saveEvent,
keyCode: 'enter',
text: 'Save',
cssClass: 'mbsc-popup-button-primary',
},
]
: [
'cancel',
{
handler: saveEvent,
keyCode: 'enter',
text: 'Add',
cssClass: 'mbsc-popup-button-primary',
},
],
[isEdit, saveEvent],
);
const popupResponsive: MbscResponsiveOptions<MbscPopupOptions> = useMemo(
() => ({
medium: {
display: 'anchored',
width: 400,
fullScreen: false,
touchUi: false,
},
}),
[],
);
const datepickerResponsive = useMemo(
() => ({
medium: {
touchUi: false,
},
}),
[],
);
const handleEventCreated = useCallback(
(args: MbscCalendarEvent) => {
setEdit(false);
setTempEvent(args.event);
loadPopupForm(args.event);
setPopupAnchor(args.target);
setPopupOpen(true);
},
[loadPopupForm],
);
const handleEventClick = useCallback(
(args: MbscCalendarEvent) => {
if (isDraggingProgress.current) {
return;
}
setEdit(true);
setTempEvent({ ...args.event });
loadPopupForm(args.event);
setPopupAnchor(args.domEvent.target);
setPopupOpen(true);
},
[loadPopupForm, isDraggingProgress],
);
const handleTitleChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
setTitle(ev.target.value);
}, []);
const handleDateChange = useCallback((args: MbscDatepickerChangeEvent) => {
setDate(args.value as MbscDateType[]);
}, []);
const handleProgressChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
setProgress(Number(ev.target.value));
}, []);
const handlePopupClose = useCallback(() => {
if (!isEdit) {
// Refresh the list, if add popup was canceled, to remove the temporary event
setMyEvents([...myEvents]);
}
setPopupOpen(false);
}, [isEdit, myEvents]);
const handleEventUpdated = useCallback(
(args: MbscCalendarEvent) => {
updateEvent(args.event);
},
[updateEvent],
);
const handleProgressArrowMouseDown = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
const target = e.target as HTMLElement | null;
const progressArrow = target!.closest('.mds-progress-arrow');
if (!progressArrow) {
return;
}
e.stopPropagation();
isDraggingProgress.current = true;
const progressBar = progressArrow.closest('.mds-progress-bar') as HTMLElement;
const progressLabel = progressArrow.closest('.mds-progress-event')?.querySelector('.mds-progress-label') as HTMLElement;
const eventContainerWidth = progressBar.parentElement?.offsetWidth ?? 0;
const initialMouseX = e.pageX;
const initialProgress = parseFloat(progressBar.style.width.replace('%', ''));
let newProgress: number;
const handleMouseMove = (e: MouseEvent) => {
const mouseXOffset = e.pageX - initialMouseX;
newProgress = Math.round(initialProgress + (mouseXOffset / eventContainerWidth) * 100);
newProgress = Math.max(0, Math.min(100, newProgress));
progressBar.style.width = `${newProgress}%`;
progressLabel.textContent = `${newProgress}%`;
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
const eventId = (progressArrow as HTMLElement).dataset.eventId;
const eventToUpdate = myEvents.find((event) => event.id === eventId);
if (eventToUpdate) {
eventToUpdate.progress = newProgress;
updateEvent(eventToUpdate);
}
setTimeout(() => {
isDraggingProgress.current = false;
}, 100);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
},
[myEvents, updateEvent],
);
const renderCustomEvent = useCallback(
(event: MbscCalendarEventData) => (
<div className="mds-progress-event" style={{ background: event.color }}>
<div className="mds-progress-bar" style={{ width: `${event.original!.progress || 0}%` }}>
<div className="mds-progress-arrow" data-event-id={event.original!.id}></div>
</div>
<div className="mds-progress-event-content">
<div className="mds-progress-event-title">{event.original!.title}</div>
</div>
<div className="mds-progress-label" key={event.original!.progress || 0}>
{event.original!.progress || 0}%
</div>
</div>
),
[],
);
const renderCustomResource = useCallback(
(resource: MbscResource) => (
<div>
<div className="mds-progress-employee-name">{resource.name}</div>
{resource.title && <div className="mds-progress-employee-title">{resource.title}</div>}
</div>
),
[],
);
return (
<div onMouseDownCapture={handleProgressArrowMouseDown}>
<Eventcalendar
class="mds-progress-calendar"
view={myView}
data={myEvents}
resources={myResources}
clickToCreate={true}
dragToCreate={true}
dragToMove={true}
dragToResize={true}
onEventClick={handleEventClick}
onEventCreated={handleEventCreated}
onEventUpdated={handleEventUpdated}
renderResource={renderCustomResource}
renderTimelineEvent={renderCustomEvent}
/>
<Popup
display="bottom"
fullScreen={true}
contentPadding={false}
headerText={popupHeaderText}
anchor={popupAnchor}
buttons={popupButtons}
isOpen={isPopupOpen}
onClose={handlePopupClose}
responsive={popupResponsive}
>
<div className="mbsc-form-group">
<Input label="Title" value={popupEventTitle} onChange={handleTitleChange} />
</div>
<div className="mbsc-form-group">
<Input ref={startInputRef} label="Starts" />
<Input ref={endInputRef} label="Ends" />
<Datepicker
select="range"
touchUi={true}
startInput={startInput}
endInput={endInput}
showRangeLabels={false}
responsive={datepickerResponsive}
onChange={handleDateChange}
value={popupEventDate}
/>
</div>
<div className="mbsc-form-group">
<label className="mbsc-flex mbsc-align-items-center mbsc-padding">
<span>Progress</span>
<input
className="mds-popup-progress-slider mbsc-flex-1-0"
type="range"
min="0"
max="100"
value={popupEventProgress}
onChange={handleProgressChange}
/>
<span className="mds-popup-progress-label">{popupEventProgress}%</span>
</label>
</div>
</Popup>
</div>
);
}
export default App;
.mds-progress-calendar .mbsc-timeline-row-gutter {
height: 6px;
}
.mds-progress-calendar .mbsc-timeline-parent {
height: 34px;
}
.mds-popup-progress-slider {
margin: 0 10px;
}
.mds-popup-progress-label {
margin-left: 10px;
width: 50px;
text-align: right;
}
.mds-progress-event {
border-radius: 4px;
position: relative;
overflow: hidden;
line-height: 19px;
}
.mds-progress-event-content {
padding: 10px 0;
margin-right: 50px;
}
.mds-progress-event-title {
position: relative;
color: white;
font-size: 14px;
padding: 0 10px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.mds-progress-bar {
position: absolute;
top: 0;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
}
.mds-progress-arrow {
position: absolute;
right: -9px;
bottom: 0;
border-style: solid;
border-width: 0 9px 9px 9px;
border-color: transparent transparent rgba(255, 255, 255, 0.5) transparent;
cursor: ew-resize;
}
.mds-progress-dragging {
background-color: rgba(0, 0, 0, 0.5);
}
.mds-progress-dragging .mds-progress-arrow,
.mds-progress-arrow:hover {
right: -12px;
border-width: 0 12px 12px 12px;
border-color: transparent transparent white transparent;
}
.mds-progress-label {
position: absolute;
right: 5px;
top: 50%;
margin-top: -10px;
color: white;
font-weight: bold;
font-size: 14px;
}
.mds-progress-employee-name {
font-size: 14px;
}
.mds-progress-employee-title {
font-size: 12px;
font-weight: 400;
margin-top: 5px;
}