GanttKit / Gantt chart component
Gantt chart component

A Gantt chart component for React, Angular and Vue

Most Gantt chart components are a framework wrapper the vendor maintains, which means you wait for their React support, their Angular support, their Vue support. GanttKit ships all three as first-party packages over one headless engine, and none of them is a private fork of the chart: the engine computes the same scene, the same feature plugins install, and the renderer stays your choice.

React 19 Angular Vue 3 Official bindings Any base renderer MIT licensed
$ pnpm add @ganttkit/core @ganttkit/react Launch live demo Read the docs

A binding is not a fourth renderer. @ganttkit/react, @ganttkit/angular and @ganttkit/vue own the renderer's lifetime and map the engine options onto the framework's reactivity. Everything else, the layout math, the plugins, the scene, is the same code a vanilla caller runs.

One engine, three bindings

PackageFrameworkExports
@ganttkit/reactReact 19GanttChart, useGantt
@ganttkit/angularAngular, Angular Package FormatGanttChartComponent, selector gantt-chart
@ganttkit/vueVue 3GanttChart, useGantt

Install one of them next to @ganttkit/core, plus whichever feature plugins you want. The base renderer defaults to @ganttkit/html, so a chart is one component and one stylesheet away.

React

import { GanttChart } from '@ganttkit/react'
import { createColumns } from '@ganttkit/plugin-columns'
import { progressPlugin } from '@ganttkit/plugin-progress'
import '@ganttkit/react/styles.css'

// Declared once, outside the component: plugins are read when the engine is built.
const plugins = [
  createColumns({ columns: [{ key: 'name', label: 'Task' }] }).plugin,
  progressPlugin(),
]

export function Schedule({ rows }) {
  return (
    <GanttChart
      rows={rows}
      plugins={plugins}
      viewMode="Week"
      theme="dark"
      style={{ height: '70vh' }}
    />
  )
}

There is no mount effect, no ref to a host element and no cleanup to remember. A new rows array is pushed into the running engine rather than remounting the chart, so the component can re-render as often as React likes.

Angular

import { Component } from '@angular/core'
import { GanttChartComponent } from '@ganttkit/angular'
import { createColumns } from '@ganttkit/plugin-columns'
import type { Row } from '@ganttkit/core'

@Component({
  selector: 'app-schedule',
  standalone: true,
  imports: [GanttChartComponent],
  template: `
    <gantt-chart
      [rows]="rows"
      [plugins]="plugins"
      viewMode="Week"
      theme="dark"
      style="height: 70vh"
    ></gantt-chart>
  `,
})
export class ScheduleComponent {
  rows: Row[] = []
  plugins = [createColumns({ columns: [{ key: 'name', label: 'Task' }] }).plugin]
}

The package is published in the Angular Package Format and the component is standalone, so it goes in imports with no module to declare. Add the stylesheet to your global styles, either as @import '@ganttkit/angular/styles.css' in styles.css or as an entry in the styles array in angular.json.

Vue 3

<script setup>
import { GanttChart } from '@ganttkit/vue'
import { createColumns } from '@ganttkit/plugin-columns'
import { progressPlugin } from '@ganttkit/plugin-progress'
import '@ganttkit/vue/styles.css'

const props = defineProps({ rows: Array })

const plugins = [
  createColumns({ columns: [{ key: 'name', label: 'Task' }] }).plugin,
  progressPlugin(),
]
</script>

<template>
  <GanttChart :rows="props.rows" :plugins="plugins" view-mode="Week" theme="dark" style="height: 70vh" />
</template>

Note that plugins is a plain array, not a ref. The engine manages its own updates, so there is nothing to gain from making the plugin list reactive and nothing to lose by leaving it out.

Which props update, which rebuild

Every binding follows the same contract, so the mental model transfers between them. The difference matters for performance: a pushed prop reuses the engine, a rebuild throws it away and builds a new one.

PropTypeOn change
rowsRow[]Pushed into the running engine
viewMode'Day' | 'Week' | 'Month'Pushed into the running engine
themetheme namePushed into the running engine
dateAdapterDateAdapterPushed into the running engine
rendererRendererFactoryRebuilds the chart
construction-time optionsrowHeight, dayWidth, draggable, virtualize, ...Rebuilds the engine
plugins, chevronplugin list, chevronRead once per engine, so a later change is not picked up

That last row is the one to design around: build the plugin array outside the render path, as a module constant or a field, so it keeps its identity. Rebuilding it inline on every render is what makes a chart feel slow.

Below the component

@ganttkit/react and @ganttkit/vue also export useGantt, a hook in React and a composable in Vue, which is the lifetime management the component is built on. Reach for it when the component's props are not enough and you need the engine itself: updateTask for a single edit instead of a whole new rows array, an events subscription for task:click or task:dragend, or a plugin command such as tree.expandAll. The API reference lists the full engine surface, and every one of those methods is the same method a vanilla caller uses.

Choosing the renderer

The three base renderers consume an identical scene, so they are drop-in swaps for one another. A vanilla caller picks one by importing it. A binding takes it as an option and defaults to htmlRenderer:

import { GanttChart } from '@ganttkit/react'
import { canvasRenderer } from '@ganttkit/canvas'
import '@ganttkit/canvas/styles.css'

// <GanttChart renderer={canvasRenderer} rows={rows} />

@ganttkit/svg and @ganttkit/canvas are optional peers of the bindings: install the one you use and import its stylesheet from that package. A binding's own styles.css re-exports the HTML sheet only, because that is the renderer it depends on. Any factory matching RendererFactory works, so a renderer you wrote yourself drops in the same way.

Svelte, Solid, or no framework at all

There is no binding for these yet, and you do not need one. The engine is headless and imperative, the renderer is a plugin, and a wrapper is about twenty lines. Three rules cover it:

  1. Create once, on mount. The engine is long-lived. Build it when the host element exists, not on every render.
  2. Keep it out of reactive state. Store the engine in a ref or a plain variable. 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.
<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>

Those reactive statements are doing by hand what a binding does for you, and this is the table they follow. Prefer the narrow call over replacing the dataset wherever you can:

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

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)

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, and each binding creates its chart in the framework's own client-only mount path. A hand-rolled wrapper has to keep that discipline itself: 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, Angular or Vue Gantt chart component?

Yes, as of v0.2.0. Three packages ship official bindings: @ganttkit/react for React 19, @ganttkit/angular as a standalone component, and @ganttkit/vue for Vue 3. Install one alongside @ganttkit/core and render a GanttChart.

Is a framework binding a different Gantt renderer?

No. The engine still computes the scene and a base renderer still paints it. The binding owns that renderer's lifetime and maps the options onto the framework's reactivity, so every feature plugin works unchanged.

Which props update the chart in place and which rebuild it?

rows, viewMode, theme and dateAdapter are pushed into the running engine. Construction-time options, and the renderer option, rebuild the engine. plugins and chevron are read once per engine, so declare them outside the render path.

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, and each binding creates the chart in its own client-only mount path.

What about Svelte, Solid or no framework at all?

Use @ganttkit/core with a base renderer directly. The engine is headless and imperative, so a wrapper is about twenty lines: create it on mount, keep it out of reactive state, and call destroy on unmount. The same wrapper works as a custom element for pages you do not control.