---
title: "React Timeline Custom Cell Content Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/dynamic-cell-content-template"
---

# Custom Cell Content - React Timeline demo

Add workload badges, status icons, and a hover quick-add button to each React Timeline cell — and cap events per cell. For React and Next.JS.

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

## Description

Dynamically customize the cell content of the timeline by showing event-related icons, visual indicators, and a quick add button directly in each cell.

This is achieved using the [`renderCell`](https://mobiscroll.com/docs/react/eventcalendar/timeline#renderer-renderCell) function, where event data in each cell is used to display icons based on the event, a badge showing total event hours, and a button to quickly add new events. The add event icon is dynamically added during [`onCellHoverIn`](https://mobiscroll.com/docs/react/eventcalendar/timeline#event-onCellHoverIn). An event cap per cell is implemented by reading the event count from [`onCellHoverIn`](https://mobiscroll.com/docs/react/eventcalendar/timeline#event-onCellHoverIn) args and enforcing it in [`onEventCreate`](https://mobiscroll.com/docs/react/eventcalendar/timeline#event-onEventCreate), blocking event creation by returning `false`. This approach helps visualize workload per day and enables quick actions, making scheduling more efficient and user-friendly.

In this demo, cells represent a day. Depending on the resolution, they can also stand for a week, month, or more, while remaining fully customizable. See this [configuration demo](https://demo.mobiscroll.com/react/timeline/multiple-days-weeks-months-quarters-years-variable-resolution) to try out different timeline resolution settings.

## What this demo shows

- A desktop monthly timeline where days are arranged horizontally and resources are listed as rows on the left.
- **Header navigation** The month and year label opens date navigation, the previous and next arrows move the visible timeline range, and the Today button returns to the current date.
- **Month view** The timeline header shows the days of the selected month, with the current date highlighted.
- **Resources** Multiple resources are displayed as separate timeline rows.
- **Custom cell content** Each resource-day cell shows scheduled hours against available working hours, such as `8h`.
- **Workload indicators** Color-coded badges show resource utilization at a glance, including fully utilized, partially utilized, and overloaded or underutilized states based on the configured rules.
- **Task overview** Individual tasks remain visible beneath the workload summary, so each cell shows both daily capacity and scheduled work.
- **Dynamic summaries** Workload totals reflect the current events in the cell when event data changes.
- **Status icons** Custom icons can represent task metadata, priorities, activity types, or other event categories.
- **Event labels** Events are displayed as blue labels with the event title and duration.
- **Event creation** A `+` button appears in the upper-right corner of a cell on hover and can be used to add a new event.
- **Date positioning** Events are placed by their assigned resource and date.
- **Event interaction** Events highlight on hover.
- **Event selection** Clicking an event selects and highlights it.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through dates and resources.

## Best for

- **Workforce scheduling** Planning daily work across multiple people, teams, or resource groups.
- **Project planning** Comparing assigned work against available capacity by resource and day.
- **Field service scheduling** Reviewing technician or crew workload across a monthly timeline.
- **Manufacturing planning** Balancing daily tasks across machines, stations, or production resources.
- **Resource management** Spotting overbooked, underused, or partially allocated resources in a timeline layout.
- **Capacity planning** Showing workload totals and task details together without leaving the calendar view.

## 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/eventcalendar/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - 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
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`renderCell`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderCell) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

```jsx
import { Eventcalendar, setOptions, Toast } from '@mobiscroll/react';
import { useCallback, useMemo, useState } from 'react';

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

function App() {
  const [isToastOpen, setToastOpen] = useState(false);
  const [toastMessage, setToastMessage] = useState();
  const [myEvents, setEvents] = useState(() => [
    {
      id: 1,
      resource: 1,
      title: 'Review',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 2,
      resource: 1,
      title: 'Demo',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 3,
      resource: 1,
      title: 'Kickoff',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 4,
      resource: 4,
      title: 'Strategy',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 5,
      resource: 4,
      title: 'Collab',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    // Showing partial data. Download full source.
  ]);

  const iconMap = useMemo(
    () => ({
      Review: 'calendar',
      Demo: 'play',
      Kickoff: 'flag',
      Strategy: 'map',
      Collab: 'bubbles',
      Update: 'upload',
      Discussion: 'bubble',
      Planning: 'pencil',
      Retrospect: 'history',
      Onboard: 'user4',
    }),
    [],
  );
  const titles = useMemo(() => Object.keys(iconMap), [iconMap]);

  const myResources = useMemo(
    () => [
      { id: 1, name: 'Resource A' },
      { id: 2, name: 'Resource B' },
      { id: 3, name: 'Resource C' },
      { id: 4, name: 'Resource D' },
    ],
    [],
  );

  const myView = useMemo(
    () => ({
      timeline: {
        endDay: 5,
        eventDisplay: 'fill',
        type: 'month',
        resolutionHorizontal: 'day',
        startDay: 1,
      },
    }),
    [],
  );

  const customDefaultEvent = useCallback(
    (args) => ({
      title: titles[Math.floor(Math.random() * titles.length)],
      end: new Date(args.start.getTime() + 2 * 3600000),
    }),
    [titles],
  );

  const handleAddClick = useCallback(
    (cell) => {
      if (cell.events.length >= 4) {
        setToastOpen(true);
        setToastMessage('Task limit reached');
        return;
      }
      setEvents((prevEvents) => [
        ...prevEvents,
        {
          start: cell.date,
          end: new Date(cell.date.getTime() + 2 * 3600000),
          resource: cell.resource.id,
          title: titles[Math.floor(Math.random() * titles.length)],
        },
      ]);
    },
    [titles],
  );

  const renderCell = useCallback(
    (cell) => {
      const events = cell.events || [];
      const hours = Math.round(events.reduce((s, ev) => s + (new Date(ev.end) - new Date(ev.start)) / 36e5, 0));

      const classMap = { 2: 'light', 4: 'medium', 6: 'semi', 8: 'full' };
      const colorClass = 'mds-timeline-cell-content-badge-' + (classMap[hours] || 'default');

      const addedIcons = new Set();
      const iconHtml = events.map((ev) => {
        const iconName = iconMap[ev.title];
        if (iconName && !addedIcons.has(ev.title)) {
          addedIcons.add(ev.title);
          return <i key={ev.title} className={'mbsc-font-icon mbsc-icon-' + iconName + ' mds-timeline-cell-icon'} title={ev.title}></i>;
        }
        return null;
      });

      return (
        <>
          <div className={'mds-timeline-cell-content-badge ' + colorClass}>{hours}h / 8h</div>
          <button onClick={() => handleAddClick(cell)} className="mds-timeline-cell-content-add">
            +
          </button>
          <div className="mds-timeline-cell-icons">{iconHtml}</div>
        </>
      );
    },
    [handleAddClick, iconMap],
  );

  const renderTimelineEventContent = useCallback((event) => {
    const hours = Math.round((event.endDate - event.startDate) / 36e5);
    return (
      <div>
        {event.title} - {hours}h
      </div>
    );
  }, []);

  return (
    <>
      <Eventcalendar
        cssClass="mds-timeline-cell-content"
        clickToCreate={false}
        data={myEvents}
        dragToCreate={false}
        dragToMove={false}
        dragToResize={false}
        extendDefaultEvent={customDefaultEvent}
        resources={myResources}
        renderCell={renderCell}
        renderTimelineEventContent={renderTimelineEventContent}
        view={myView}
      />
      <Toast isOpen={isToastOpen} message={toastMessage} onClose={() => setToastOpen(false)} />
    </>
  );
}

export default App;
```

### TSX

```tsx
import {
  Eventcalendar,
  MbscCalendarCellData,
  MbscCalendarEvent,
  MbscCalendarEventData,
  MbscEventcalendarView,
  MbscNewEventData,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useState } from 'react';

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

const App: FC = () => {
  const [isToastOpen, setToastOpen] = useState<boolean>(false);
  const [toastMessage, setToastMessage] = useState<string>();
  const [myEvents, setEvents] = useState<MbscCalendarEvent[]>(() => [
    {
      id: 1,
      resource: 1,
      title: 'Review',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 2,
      resource: 1,
      title: 'Demo',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 3,
      resource: 1,
      title: 'Kickoff',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 4,
      resource: 4,
      title: 'Strategy',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    {
      id: 5,
      resource: 4,
      title: 'Collab',
      start: '2026-11-01T00:00',
      end: '2026-11-01T02:00',
    },
    // Showing partial data. Download full source.
  ]);

  const iconMap = useMemo(
    () => ({
      Review: 'calendar',
      Demo: 'play',
      Kickoff: 'flag',
      Strategy: 'map',
      Collab: 'bubbles',
      Update: 'upload',
      Discussion: 'bubble',
      Planning: 'pencil',
      Retrospect: 'history',
      Onboard: 'user4',
    }),
    [],
  );
  const titles = useMemo(() => Object.keys(iconMap), [iconMap]);

  const myResources = useMemo(
    () => [
      { id: 1, name: 'Resource A' },
      { id: 2, name: 'Resource B' },
      { id: 3, name: 'Resource C' },
      { id: 4, name: 'Resource D' },
    ],
    [],
  );

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      timeline: {
        endDay: 5,
        eventDisplay: 'fill',
        type: 'month',
        resolutionHorizontal: 'day',
        startDay: 1,
      },
    }),
    [],
  );

  const customDefaultEvent = useCallback(
    (args: MbscNewEventData) => ({
      title: titles[Math.floor(Math.random() * titles.length)],
      end: new Date(args.start.getTime() + 2 * 3600000),
    }),
    [titles],
  );

  const handleAddClick = useCallback(
    (cell: MbscCalendarCellData) => {
      if (cell.events.length >= 4) {
        setToastOpen(true);
        setToastMessage('Task limit reached');
        return;
      }

      setEvents((prevEvents) => [
        ...prevEvents,
        {
          start: cell.date,
          end: new Date(cell.date.getTime() + 2 * 3600000),
          resource: cell.resource.id,
          title: titles[Math.floor(Math.random() * titles.length)],
        } as MbscCalendarEvent,
      ]);
    },
    [titles],
  );

  const renderCell = useCallback(
    (cell: MbscCalendarCellData) => {
      const events = cell.events || [];
      const hours = Math.round(
        (cell.events || []).reduce(
          (sum, ev) =>
            ev.start && ev.end ? sum + (new Date(ev.end as Date).getTime() - new Date(ev.start as Date).getTime()) / 36e5 : sum,
          0,
        ),
      );

      const classMap = { 2: 'light', 4: 'medium', 6: 'semi', 8: 'full' };
      const colorClass = 'mds-timeline-cell-content-badge-' + (classMap[hours as keyof typeof classMap] || 'default');

      const addedIcons = new Set();
      const iconHtml = events.map((ev) => {
        const iconName = iconMap[ev.title as keyof typeof iconMap];
        if (iconName && !addedIcons.has(ev.title)) {
          addedIcons.add(ev.title);
          return <i key={ev.title} className={'mbsc-font-icon mbsc-icon-' + iconName + ' mds-timeline-cell-icon'} title={ev.title}></i>;
        }
        return null;
      });

      return (
        <>
          <div className={'mds-timeline-cell-content-badge ' + colorClass}>{hours}h / 8h</div>
          <button onClick={() => handleAddClick(cell)} className="mds-timeline-cell-content-add">
            +
          </button>
          <div className="mds-timeline-cell-icons">{iconHtml}</div>
        </>
      );
    },
    [handleAddClick, iconMap],
  );

  const renderTimelineEventContent = useCallback((event: MbscCalendarEventData) => {
    const hours = Math.round(((event.endDate as Date).getTime() - (event.startDate as Date).getTime()) / 36e5);
    return (
      <div>
        {event.title} - {hours}h
      </div>
    );
  }, []);

  return (
    <>
      <Eventcalendar
        cssClass="mds-timeline-cell-content"
        clickToCreate={false}
        data={myEvents}
        dragToCreate={false}
        dragToMove={false}
        dragToResize={false}
        extendDefaultEvent={customDefaultEvent}
        resources={myResources}
        renderCell={renderCell}
        renderTimelineEventContent={renderTimelineEventContent}
        view={myView}
      />
      <Toast isOpen={isToastOpen} message={toastMessage} onClose={() => setToastOpen(false)} />
    </>
  );
};

export default App;
```

### CSS

```css
.mds-timeline-cell-content .mbsc-timeline-day-fri::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  border-right: 1px solid var(--mbsc-eventcalendar-border-color);
}

.mds-timeline-cell-content .mbsc-timeline-day-fri.mbsc-ltr::after {
  left: 100%;
}

.mds-timeline-cell-content .mbsc-timeline-day-fri.mbsc-rtl::after {
  right: 100%;
}

.mds-timeline-cell-content .mbsc-timeline-events {
  top: 25px;
}

.mds-timeline-cell-content .mbsc-timeline-row-gutter {
  height: 55px;
}

.mds-timeline-cell-content .mbsc-timeline-column {
  width: 110px;
}

.mds-timeline-cell-content-badge {
  position: relative;
  top: 6px;
  left: 4px;
  display: inline-block;
  padding: 1px 4px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 12px;
  max-width: 80px;
  color: black;
  background: #f9f9f9;
  overflow: hidden;
  z-index: 0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}

.mds-timeline-cell-content-badge::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  transform: skewX(-15deg);
  z-index: -1;
}

.mds-timeline-cell-content-badge-light::before {
  width: 25%;
  background: #ff9c9c;
}

.mds-timeline-cell-content-badge-medium::before {
  width: 50%;
  background: #ffcf9f;
}

.mds-timeline-cell-content-badge-semi::before {
  width: 75%;
  background: #fff4a3;
}

.mds-timeline-cell-content-badge-full::before {
  width: 100%;
  background: #a3e4a1;
}

.mds-timeline-cell-content-add {
  opacity: 0;
  position: absolute;
  top: 6px;
  width: 17px;
  height: 17px;
  line-height: 13px;
  padding: 0;
  padding-bottom: 4px;
  color: #fff;
  border: none;
  border-radius: 50%;
  font-size: 22px;
  cursor: pointer;
  background: linear-gradient(135deg, rgb(73, 73, 73), rgb(22, 21, 21));
  transition: transform 0.2s ease;
}

.mbsc-timeline-column.mbsc-ltr .mds-timeline-cell-content-add {
  right: 4px;
}

.mbsc-timeline-column.mbsc-rtl .mds-timeline-cell-content-add {
  left: 4px;
}

.mbsc-timeline-column:hover .mds-timeline-cell-content-add {
  opacity: 1;
  pointer-events: auto;
}

.mds-timeline-cell-content-add:hover {
  outline: 3px solid rgba(140, 140, 140, 0.5);
}

.mds-timeline-cell-icons {
  position: absolute;
  inset: auto auto 4px auto;
}

.mds-timeline-cell-icon {
  text-align: center;
  padding: 2px 4px;
  margin: 0 3px;
  line-height: 17px !important;
  color: #000;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 5px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  font-size: 13px;
}
```

## Implementation notes

- Use `timeline: { type: 'month', resolutionHorizontal: 'day', startDay: 1, endDay: 5, eventDisplay: 'fill' }` - a monthly timeline with one column per day, Mon-Fri only, events listed as fills rather than positioned by time.
- Define 4 plain resources (Resource A-D) with only `id` and `name` - no `color` needed. Events have no `color` either; the cell badge provides the workload indicator instead.
- Build an `iconMap` that maps event title strings to Mobiscroll icon names (e.g. `Review → 'calendar'`, `Demo → 'play'`, `Kickoff → 'flag'`). Use the keys of this map as the pool of valid event titles.
- Use `renderCell` (Angular: `cellTemplate`, Vue: `#cell`) to inject content into each timeline cell. The render function receives a `cell` object with `cell.events` (the events on that cell), `cell.date`, and `cell.resource`. From `cell.events`, compute the total scheduled hours by summing each event's duration, and render: a workload badge showing `{hours}h / 8h`, a "+" add button, and a row of deduplicated event-type icons (one icon per distinct title using `mbsc-icon-{iconName}`).
- Use `renderTimelineEventContent` (Angular: `timelineEventContentTemplate`, Vue: `#timelineEventContent`) to render a compact event label showing the event title and its duration in hours, calculated from `event.endDate` and `event.startDate`.
- In the "+" button's click handler, read `cell.events.length`. If it is 4 or more, show a `Toast` with "Task limit reached" and return early. Otherwise, add a new event to the events array with `start: cell.date`, `end: 2 hours later`, `resource: cell.resource.id`, and a random title from the titles pool. In Angular, call `stopPropagation()` on the click event to prevent the calendar from handling it.
- Use `extendDefaultEvent` returning a random title from the titles pool and an `end` 2 hours after `args.start`, so the same defaults apply to any events created via other means.
- Disable all built-in interaction: set `clickToCreate`, `dragToCreate`, `dragToMove`, and `dragToResize` all to `false`.

## 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: [Event Appearance & Time Buffers](https://demo.mobiscroll.com/react/timeline/timeline-custom-event-rendering.md) - Customize event layouts on a React Timeline with icons, labels, and colors — visualize before/after buffers for travel, prep, or inspection workflows.
- Next: [Header and Footer Template](https://demo.mobiscroll.com/react/timeline/hour-day-week-month-quarter-year-header-footer-template.md) - Customize time-scale headers and add occupancy footers to a React Timeline — format cells from hour to year and display per-slot utilization.
