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
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
- Create once, on mount. The engine is imperative and long-lived. Build it when the element exists, not on every render.
- 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.
- 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 change | Engine call | Cost |
|---|
| rows | setRows(rows) | Full recompute, about 8 ms at 20,000 tasks |
| one task edited | updateTask(id, patch) | Cheaper than replacing the dataset |
| view mode or zoom | setViewMode(mode) | Recomputes the time scale and layout |
| selection | selectTask(id) | Scene-only update |
| unmount | destroy() | 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
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.