---
title: "React Timeline Task Progress Tracking Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/show-task-progress-on-event"
---

# Task Progress Tracking - React Timeline demo

Show task completion progress on the React Timeline events with an inline hover slider for live updates and a progress field in the add/edit dialog. For React and Next.JS.

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

## Description

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`](https://mobiscroll.com/docs/react/eventcalendar/timeline#renderer-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.

## What this demo shows

- A desktop monthly timeline for project planning and progress tracking, with days arranged horizontally and resources listed vertically on the left.
- **Header navigation** The month and year label opens date navigation. The previous and next arrows and the Today button move between months and return to the current day.
- **Timeline header** The fixed date strip shows the days of the selected month in `D DDD` format, with the current date highlighted.
- **Resources** Hierarchical resource groups organize team members into departments such as Designer Team, Development Team, and QA Team.
- **Resource details** Individual resources show custom information, including the team member's role or job title.
- **Event colors** Events for the same team member or group keep a consistent color in the timeline.
- **Event cards** Events are rendered as colored cards with the project name, a progress percentage, and an inline progress slider.
- **Inline progress** The progress slider reacts on hover and lets users update completion directly on the event.
- **Event positioning** Events are placed by assigned resource and by start and end date.
- **Event creation** Users can create events by double-clicking a resource row or by clicking and dragging across a date range.
- **Add form** Creating an event opens a form with title, start date, end date, and progress slider fields, plus Cancel and Add actions.
- **Event editing** Clicking an event selects it and opens a popup for modifying the event details.
- **Drag and resize** Events show drag and resize handles on hover for moving tasks or changing their duration.
- **Vertical scrolling** The resource area scrolls vertically so the timeline can display more resources in the same view.

## Best for

- **Project management** Tracking tasks, ownership, timing, and completion progress in a single timeline.
- **Sprint planning** Planning work across team members and date ranges while keeping progress visible.
- **Software development teams** Coordinating designers, developers, and QA resources across overlapping project tasks.
- **Task tracking** Updating task progress directly from the timeline without opening a separate detail view.
- **Team workload management** Reviewing who is assigned to what, when each task runs, and how far along the work is.
- **Gantt-style planning** Showing project spans and completion percentages without requiring a full Gantt chart implementation.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/react/popup/api#opt-cssClass) - option
- [`display`](https://mobiscroll.com/docs/react/popup/api#opt-display) - option
- [`width`](https://mobiscroll.com/docs/react/popup/api#opt-width) - option
- [`fullScreen`](https://mobiscroll.com/docs/react/popup/api#opt-fullScreen) - option
- [`touchUi`](https://mobiscroll.com/docs/react/popup/api#opt-touchUi) - option
- [`touchUi`](https://mobiscroll.com/docs/react/datepicker/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-width) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`onEventClick`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdated) - event
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`renderTimelineEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineEvent) - renderer
- [`headerText`](https://mobiscroll.com/docs/react/popup/api#opt-headerText) - option
- [`anchor`](https://mobiscroll.com/docs/react/popup/api#opt-anchor) - option
- [`buttons`](https://mobiscroll.com/docs/react/popup/api#opt-buttons) - option
- [`isOpen`](https://mobiscroll.com/docs/react/popup/api#opt-isOpen) - option
- [`onClose`](https://mobiscroll.com/docs/react/popup/api#event-onClose) - event
- [`responsive`](https://mobiscroll.com/docs/react/popup/api#opt-responsive) - option
- [`select`](https://mobiscroll.com/docs/react/datepicker/api#opt-select) - option
- [`startInput`](https://mobiscroll.com/docs/react/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/react/datepicker/api#opt-endInput) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/react/datepicker/api#opt-showRangeLabels) - option
- [`responsive`](https://mobiscroll.com/docs/react/datepicker/api#opt-responsive) - option
- [`onChange`](https://mobiscroll.com/docs/react/datepicker/api#event-onChange) - event

## Code

### JSX

```jsx
import {
  Datepicker,
  Eventcalendar,
  Input,
  Popup,
  setOptions,
} from '@mobiscroll/react';
import { useCallback, useMemo, useRef, useState } from 'react';

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

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;
```

### TSX

```tsx
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({
  theme: 'ios',
  themeVariant: 'light'
});

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;
```

### CSS

```css
.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;
}
```

## Implementation notes

- Use `type: 'month'` with `eventDisplay: 'fill'` for a month-level Gantt-style view where tasks span multiple days.
- Give each event a custom `progress` property (0-100) representing task completion percentage.
- Define resources as a two-level tree of team groups (`eventCreation: false`) and individual members with a custom `title` (job role). Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to show the member name and role beneath it.
- Use `renderTimelineEvent` (Angular: `timelineEventTemplate`, Vue: `#timelineEvent` slot) to render the progress bar layout: a container styled with the resource color, a progress fill div with `width` set to `${progress}%`, a draggable arrow handle element with a `data-event-id` attribute, the task title, and a percentage label.
- Implement inline progress dragging by attaching `onMouseDownCapture` on the calendar wrapper. On mousedown on the arrow handle, call `stopPropagation` to prevent Mobiscroll from treating it as an event click or drag, then attach `mousemove`/`mouseup` listeners to the document. On mousemove, compute new progress from the x-offset relative to the event container width. On mouseup, write the new value to the event in state and remove the listeners.
- Track drag state with an `isDraggingProgress` ref. In `onEventClick`, return early if `isDraggingProgress.current` is true so the edit popup does not open when the user finishes dragging the handle. Use a short `setTimeout` before resetting the ref to prevent the mouseup from immediately triggering a click.
- Enable `clickToCreate: true` and `dragToCreate: true` for event creation. Use `onEventCreated` to open the add popup with `args.target` as the anchor. Use `onEventClick` to open the edit popup with `args.domEvent.target` as the anchor. Use `onEventUpdated` to persist drag-to-move and drag-to-resize changes.
- Build the popup with `Popup`; default to full-screen bottom sheet on mobile and switch to an anchored 400px popup at the medium breakpoint.
- Include a `Datepicker` in range mode with `startInput`/`endInput` refs for editing the task date range, and a native `<input type="range">` (0-100) for updating the progress value directly in the form.
- On popup close when the add form is canceled, refresh the events list to remove the temporary placeholder event.

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

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 react --trial`.

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

## Related demos

- Previous: [Dual Synchronized Flight Scheduling](https://demo.mobiscroll.com/react/timeline/flight-scheduling-two-synchronized-timelines.md) - Use synchronized React Timeline views to build a flight scheduling UI — drag flights to assign them to aircraft, with fixed event times and conflict detection.
- Next: [Shifts and Tasks](https://demo.mobiscroll.com/react/timeline/tasks-subtasks-under-shifts.md) - Display shifts and linked tasks on a React Timeline — group tasks under parent shifts and restrict movement to each shift's time range.
