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

# Custom Cell Content - Vue Timeline demo

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

- [Live demo](https://demo.mobiscroll.com/vue/timeline/dynamic-cell-content-template)
- [View SFC JS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue/tree/main/src/demos/eventcalendar/timeline/dynamic-cell-content-template)
- [View SFC TS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue-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), [React](https://demo.mobiscroll.com/react/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 [`cell`](https://mobiscroll.com/docs/vue/eventcalendar/timeline#slot-cell) slot, 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/vue/eventcalendar/timeline#event-onCellHoverIn). An event cap per cell is implemented by reading the event count from [`onCellHoverIn`](https://mobiscroll.com/docs/vue/eventcalendar/timeline#event-onCellHoverIn) args and enforcing it in [`onEventCreate`](https://mobiscroll.com/docs/vue/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/vue/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/vue/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`dragToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToResize) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-extendDefaultEvent) - option
- [`resources`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`close`](https://mobiscroll.com/docs/vue/notifications/toast#event-onClose) - event

## Code

### SFC JS

```vue
<script setup>
import { MbscEventcalendar, MbscToast, setOptions } from '@mobiscroll/vue'
import { ref } from 'vue'

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

const iconMap = {
  Review: 'calendar',
  Demo: 'play',
  Kickoff: 'flag',
  Strategy: 'map',
  Collab: 'bubbles',
  Update: 'upload',
  Discussion: 'bubble',
  Planning: 'pencil',
  Retrospect: 'history',
  Onboard: 'user4'
}
const titles = Object.keys(iconMap)
const isToastOpen = ref(false)
const toastMessage = ref('')

const myEvents = ref([
  {
    id: 1,
    resource: 1,
    title: 'Review',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 2,
    resource: 1,
    title: 'Demo',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 3,
    resource: 1,
    title: 'Kickoff',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 4,
    resource: 4,
    title: 'Strategy',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 5,
    resource: 4,
    title: 'Collab',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  // Showing partial data. Download full source.
])

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

const myView = {
  timeline: {
    endDay: 5,
    eventDisplay: 'fill',
    type: 'month',
    resolutionHorizontal: 'day',
    startDay: 1
  }
}

function getHours(events = []) {
  const total = events.reduce((s, ev) => {
    const st = new Date(ev.start).getTime()
    const en = new Date(ev.end).getTime()
    return s + (en - st) / 36e5
  }, 0)
  return Math.round(total)
}

function getBadgeClass(events = []) {
  const hours = getHours(events)
  const map = { 2: 'light', 4: 'medium', 6: 'semi', 8: 'full' }
  return 'mds-timeline-cell-content-badge-' + (map[hours] || 'default')
}

function getIcons(events = []) {
  const titles = new Set()
  const icons = []
  for (const ev of events) {
    const name = iconMap[ev.title]
    if (name && !titles.has(ev.title)) {
      titles.add(ev.title)
      icons.push(name)
    }
  }
  return icons
}

function handleAddClick(cell) {
  if (cell.events.length >= 4) {
    toastMessage.value = 'Task limit reached'
    isToastOpen.value = true
    return
  }

  myEvents.value = [
    ...myEvents.value,
    {
      title: titles[Math.floor(Math.random() * titles.length)],
      start: cell.date,
      end: new Date(cell.date.getTime() + 2 * 3600000),
      resource: cell.resource.id
    }
  ]
}

function extendDefaultEvent(args) {
  return {
    title: titles[Math.floor(Math.random() * titles.length)],
    end: new Date(args.start.getTime() + 2 * 3600000)
  }
}
</script>

<template>
  <MbscEventcalendar
    cssClass="mds-timeline-cell-content"
    :clickToCreate="false"
    :data="myEvents"
    :dragToCreate="false"
    :dragToMove="false"
    :dragToResize="false"
    :extendDefaultEvent="extendDefaultEvent"
    :resources="myResources"
    :view="myView"
  >
    <template #cell="cell">
      <div :class="['mds-timeline-cell-content-badge', getBadgeClass(cell.events)]">
        {{ getHours(cell.events) }}h / 8h
      </div>

      <button class="mds-timeline-cell-content-add" @click.stop="handleAddClick(cell)">+</button>

      <div class="mds-timeline-cell-icons">
        <i
          v-for="icon in getIcons(cell.events)"
          :key="icon"
          :class="['mbsc-font-icon', 'mds-timeline-cell-icon', 'mbsc-icon-' + icon]"
        />
      </div>
    </template>

    <template #timelineEventContent="{ title, startDate, endDate }">
      <div>{{ title }} - {{ Math.round((endDate - startDate) / 36e5) }}h</div>
    </template>
  </MbscEventcalendar>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="isToastOpen = false" />
</template>

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

### SFC TS

```vue
<script setup lang="ts">
import { MbscEventcalendar, MbscToast, setOptions } from '@mobiscroll/vue'
import type {
  MbscCalendarCellData,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscNewEventData,
  MbscResource
} from '@mobiscroll/vue'
import { ref } from 'vue'

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

const iconMap: Record<string, string> = {
  Review: 'calendar',
  Demo: 'play',
  Kickoff: 'flag',
  Strategy: 'map',
  Collab: 'bubbles',
  Update: 'upload',
  Discussion: 'bubble',
  Planning: 'pencil',
  Retrospect: 'history',
  Onboard: 'user4'
}
const titles = Object.keys(iconMap)
const isToastOpen = ref<boolean>(false)
const toastMessage = ref<string>('')

const myEvents = ref<MbscCalendarEvent[]>([
  {
    id: 1,
    resource: 1,
    title: 'Review',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 2,
    resource: 1,
    title: 'Demo',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 3,
    resource: 1,
    title: 'Kickoff',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 4,
    resource: 4,
    title: 'Strategy',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  {
    id: 5,
    resource: 4,
    title: 'Collab',
    start: '2026-11-02T00:00',
    end: '2026-11-02T02:00'
  },
  // Showing partial data. Download full source.
])

const myResources: MbscResource[] = [
  { id: 1, name: 'Resource A' },
  { id: 2, name: 'Resource B' },
  { id: 3, name: 'Resource C' },
  { id: 4, name: 'Resource D' }
]

const myView: MbscEventcalendarView = {
  timeline: {
    endDay: 5,
    eventDisplay: 'fill',
    type: 'month',
    resolutionHorizontal: 'day',
    startDay: 1
  }
}

function getHours(events: MbscCalendarEvent[] = []) {
  const total = events.reduce((s, ev) => {
    const st = new Date(ev.start as Date).getTime()
    const en = new Date(ev.end as Date).getTime()
    return s + (en - st) / 36e5
  }, 0)
  return Math.round(total)
}

function getBadgeClass(events: MbscCalendarEvent[] = []) {
  const hours = getHours(events)
  const map: Record<number, string> = { 2: 'light', 4: 'medium', 6: 'semi', 8: 'full' }
  return 'mds-timeline-cell-content-badge-' + (map[hours] || 'default')
}

function getIcons(events: Array<{ title?: string }> = []) {
  const seen = new Set<string>()
  const icons: string[] = []

  for (const ev of events) {
    const t = ev.title
    if (!t) continue

    const name = iconMap[t]
    if (name && !seen.has(t)) {
      seen.add(t)
      icons.push(name)
    }
  }
  return icons
}

function handleAddClick(cell: MbscCalendarCellData) {
  if (cell.events.length >= 4) {
    toastMessage.value = 'Task limit reached'
    isToastOpen.value = true
    return
  }

  myEvents.value = [
    ...myEvents.value,
    {
      title: titles[Math.floor(Math.random() * titles.length)],
      start: cell.date,
      end: new Date(cell.date.getTime() + 2 * 3600000),
      resource: cell.resource.id
    }
  ]
}

function extendDefaultEvent(args: MbscNewEventData) {
  return {
    title: titles[Math.floor(Math.random() * titles.length)],
    end: new Date(args.start.getTime() + 2 * 3600000)
  }
}
</script>

<template>
  <MbscEventcalendar
    cssClass="mds-timeline-cell-content"
    :clickToCreate="false"
    :data="myEvents"
    :dragToCreate="false"
    :dragToMove="false"
    :dragToResize="false"
    :extendDefaultEvent="extendDefaultEvent"
    :resources="myResources"
    :view="myView"
  >
    <template #cell="cell">
      <div :class="['mds-timeline-cell-content-badge', getBadgeClass(cell.events)]">
        {{ getHours(cell.events) }}h / 8h
      </div>

      <button class="mds-timeline-cell-content-add" @click.stop="handleAddClick(cell)">+</button>

      <div class="mds-timeline-cell-icons">
        <i
          v-for="icon in getIcons(cell.events)"
          :key="icon"
          :class="['mbsc-font-icon', 'mds-timeline-cell-icon', 'mbsc-icon-' + icon]"
        />
      </div>
    </template>

    <template #timelineEventContent="{ title, startDate, endDate }">
      <div>{{ title }} - {{ Math.round((endDate - startDate) / 36e5) }}h</div>
    </template>
  </MbscEventcalendar>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="isToastOpen = false" />
</template>

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

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

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

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

## Related demos

- Previous: [Event Appearance & Time Buffers](https://demo.mobiscroll.com/vue/timeline/timeline-custom-event-rendering.md) - Customize event layouts on a Vue 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/vue/timeline/hour-day-week-month-quarter-year-header-footer-template.md) - Customize time-scale headers and add occupancy footers to a Vue Timeline — format cells from hour to year and display per-slot utilization.
