GanttKit / Gantt chart component
Gantt chart component

Build the Gantt chart component your framework wants

Most Gantt chart components are a framework wrapper the vendor maintains, which means you wait for their React 18 support, their Vue 3 support, and their Angular signals support. GanttKit inverts that: the engine is headless, the renderer is a plugin, and the component is yours in about twenty lines.

React Vue Svelte Angular Web components No adapter lock-in
$ pnpm add @ganttkit/core @ganttkit/svg Launch live demo Read the docs

There is no official framework adapter package yet. That is the point of this page: the wrapper is small enough that owning it beats waiting for one, and it never blocks a framework upgrade.

The three rules of a wrapper component

  1. Create once, on mount. The engine is imperative and long-lived. Build it when the element exists, not on every render.
  2. Keep it out of reactive state. Store the engine in a ref. Putting an imperative object into component state causes re-render loops and stale closures.
  3. Destroy on unmount. engine.destroy() removes listeners and disposes plugins in reverse install order.

React

import { useEffect, useRef } from 'react'
import { GanttEngine } from '@ganttkit/core'
import { svgRenderer } from '@ganttkit/svg'
import { createColumns } from '@ganttkit/plugin-columns'
import '@ganttkit/svg/styles.css'

export function GanttChart({ rows, viewMode = 'Week' }) {
  const hostRef = useRef(null)
  const engineRef = useRef(null)

  useEffect(() => {
    const engine = new GanttEngine({ rows, viewMode })
    engine.use(svgRenderer({ target: hostRef.current, theme: 'dark' }))
    engine.use(createColumns({ columns: [{ key: 'name', label: 'Task' }] }).plugin)
    engineRef.current = engine
    return () => engine.destroy()
  }, [])

  useEffect(() => { engineRef.current?.setRows(rows) }, [rows])
  useEffect(() => { engineRef.current?.setViewMode(viewMode) }, [viewMode])

  return <div ref={hostRef} style={{ height: 480 }} />
}

Note the empty dependency array on the first effect: the chart is created once. Prop changes flow through the engine's own mutation methods instead of tearing the chart down and rebuilding it.

Vue 3

<script setup>
import { onMounted, onBeforeUnmount, ref, watch, shallowRef } from 'vue'
import { GanttEngine } from '@ganttkit/core'
import { svgRenderer } from '@ganttkit/svg'
import '@ganttkit/svg/styles.css'

const props = defineProps({ rows: Array, viewMode: { type: String, default: 'Week' } })
const host = ref(null)
const engine = shallowRef(null)

onMounted(() => {
  engine.value = new GanttEngine({ rows: props.rows, viewMode: props.viewMode })
  engine.value.use(svgRenderer({ target: host.value, theme: 'dark' }))
})

watch(() => props.rows, (rows) => engine.value?.setRows(rows), { deep: true })
watch(() => props.viewMode, (mode) => engine.value?.setViewMode(mode))

onBeforeUnmount(() => engine.value?.destroy())
</script>

<template>
  <div ref="host" style="height: 480px"></div>
</template>

shallowRef matters here. A deep ref would make Vue walk the entire engine graph on every access, which is wasted work on an object that manages its own updates.

Svelte

<script>
  import { onMount, onDestroy } from 'svelte'
  import { GanttEngine } from '@ganttkit/core'
  import { svgRenderer } from '@ganttkit/svg'
  import '@ganttkit/svg/styles.css'

  export let rows = []
  export let viewMode = 'Week'

  let host
  let engine

  onMount(() => {
    engine = new GanttEngine({ rows, viewMode })
    engine.use(svgRenderer({ target: host, theme: 'dark' }))
  })

  $: engine?.setRows(rows)
  $: engine?.setViewMode(viewMode)

  onDestroy(() => engine?.destroy())
</script>

<div bind:this={host} style="height: 480px"></div>

Angular

@Component({
  selector: 'gantt-chart',
  standalone: true,
  template: '<div #host style="height: 480px"></div>',
})
export class GanttChartComponent implements AfterViewInit, OnChanges, OnDestroy {
  @ViewChild('host') host!: ElementRef<HTMLDivElement>
  @Input() rows: Row[] = []
  @Input() viewMode: ViewMode = 'Week'

  private engine?: GanttEngine

  ngAfterViewInit() {
    this.engine = new GanttEngine({ rows: this.rows, viewMode: this.viewMode })
    this.engine.use(svgRenderer({ target: this.host.nativeElement, theme: 'dark' }))
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes['rows']) this.engine?.setRows(this.rows)
    if (changes['viewMode']) this.engine?.setViewMode(this.viewMode)
  }

  ngOnDestroy() { this.engine?.destroy() }
}

A framework-free custom element

If the chart has to drop into pages you do not control, wrap it as a custom element once and use it anywhere, including inside any framework.

class GanttChartElement extends HTMLElement {
  connectedCallback() {
    this.engine = new GanttEngine({ rows: this.rows ?? [], viewMode: 'Week' })
    this.engine.use(svgRenderer({ target: this, theme: 'dark' }))
  }
  disconnectedCallback() { this.engine?.destroy() }
  set data(rows) { this.rows = rows; this.engine?.setRows(rows) }
}

customElements.define('gantt-chart', GanttChartElement)

Wiring props to the engine

Prop changeEngine callCost
rowssetRows(rows)Full recompute, about 8 ms at 20,000 tasks
one task editedupdateTask(id, patch)Cheaper than replacing the dataset
view mode or zoomsetViewMode(mode)Recomputes the time scale and layout
selectionselectTask(id)Scene-only update
unmountdestroy()Removes listeners, disposes plugins

Server-side rendering

The core has no DOM access and no browser globals, so it is safe to import and even compute with on the server. Renderers mount on the client, so keep chart creation inside the client-only mount path: useEffect in React, onMounted in Vue, onMount in Svelte, ngAfterViewInit in Angular.

Keep reading

JavaScript Gantt chart

Install from npm and render with plain JavaScript, no framework required.

Read more

TypeScript Gantt chart

Typed rows, options, scene primitives and plugin authoring.

Read more

Gantt chart library

The architecture, the package map, and how to compare Gantt libraries.

Read more

Open source Gantt chart

MIT licensed, no keys, no telemetry, no gated features.

Read more

Frequently asked questions

Is there an official React or Vue Gantt chart component?

Not yet. Because the engine is headless and the renderer is a plugin, a wrapper component is about twenty lines: create the engine on mount, install the renderer, and call destroy on unmount.

How does a component keep the chart in sync with props?

Keep the engine in a ref rather than in component state. When the rows prop changes call setRows, when the view mode prop changes call setViewMode. The engine recomputes and the renderer repaints without remounting.

Does the Gantt chart component work with server-side rendering?

The core never touches the DOM, so it computes safely on the server. Renderers mount on the client, so create the chart inside a client-only mount effect.

How do I avoid memory leaks when the component unmounts?

Always call engine.destroy() in the unmount path. It removes listeners and disposes plugins in reverse install order.