GanttKit / TypeScript Gantt chart
TypeScript Gantt chart

A TypeScript Gantt chart with types that ship in the box

GanttKit is written for TypeScript consumers: rows, tasks, engine options, scene primitives and the plugin contract are all exported interfaces. Definitions ship inside each package, so there is no separate types package to install and nothing to keep in sync.

Types included 0 core dependencies ESM + CJS Typed plugin API Testable in Node MIT
$ pnpm add @ganttkit/core @ganttkit/svg API and types Launch live demo

The types you will use first

The data model is two interfaces. Both are exported, so you can type your API responses against them instead of writing a mapping layer.

type ViewMode = 'Day' | 'Week' | 'Month'

interface Task {
  id: string
  name: string
  start: string | Date
  end: string | Date            // inclusive
  kind?: 'task' | 'milestone'
  progress?: number             // 0 to 1
  dependencies?: string[]       // predecessor task ids
  className?: string
  draggable?: boolean
}

interface Row {
  id: string
  name: string
  tasks: Task[]
  level?: number
  parentId?: string
  hasChildren?: boolean
  expanded?: boolean
  [key: string]: unknown        // extra fields stay readable
}

The index signature on Row is deliberate: attach your own domain fields (owner, budget, status) and read them back in a column formatter without casting through any.

Typed engine options

Options are a single interface with documented defaults, so the compiler catches a misspelled key instead of silently ignoring it at runtime.

interface GanttOptions {
  rows?: Row[]
  viewMode?: ViewMode          // 'Week'
  startDate?: string | Date | null
  endDate?: string | Date | null
  rowHeight?: number           // 50
  dayWidth?: number            // 60
  barPadding?: number          // 6
  highlightToday?: boolean     // true
  draggable?: boolean          // true
  virtualize?: boolean         // true
  overscanRows?: number        // 4
  overscanCols?: number        // 6
  dateAdapter?: DateAdapter
}

A typed chart

import { GanttEngine, type Row, type ViewMode } from '@ganttkit/core'
import { svgRenderer } from '@ganttkit/svg'
import '@ganttkit/svg/styles.css'

interface ProjectRow extends Row {
  owner: string
}

const rows: ProjectRow[] = [
  {
    id: 'design',
    name: 'Design',
    owner: 'ana',
    tasks: [
      { id: 't1', name: 'Wireframes', start: '2026-07-01', end: '2026-07-08', progress: 1 },
    ],
  },
]

const mode: ViewMode = 'Week'
const engine = new GanttEngine({ rows, viewMode: mode })

engine.use(svgRenderer({ target: '#chart', theme: 'dark' }))

Mutations are typed too

Patching a task takes a Partial<Task>, so an invalid field or a wrong value type is a compile error rather than a silent no-op.

engine.updateTask('t1', { progress: 0.5 })                      // ok
engine.updateTask('t1', { progres: 0.5 })                       // compile error
engine.updateTaskDates('t1', '2026-07-02', '2026-07-09')        // ok
engine.setViewMode('Sprint')                                    // compile error, not a ViewMode

The scene is a typed data structure

The engine emits a scene of vector primitives rather than DOM. That scene is fully typed, which is what makes a custom renderer or a snapshot test practical.

interface Scene {
  width: number
  height: number
  layers: SceneLayer[]
}

interface SceneLayer { name: string; primitives: ScenePrimitive[] }

type ScenePrimitive = RectPrim | LinePrim | PathPrim | PolygonPrim | TextPrim

Because ScenePrimitive is a discriminated union on type, a renderer that switches over it gets exhaustiveness checking for free: add a primitive kind and every unhandled switch lights up.

Writing a plugin in TypeScript

A plugin is an object with a name and an install function. The context it receives carries the typed registries: store, events, commands, services, ui and hooks. Services are consumed with a generic parameter, so a capability published by one plugin arrives typed in another.

import type { Plugin } from '@ganttkit/core'

export function highlightOverdue(): Plugin {
  return {
    name: 'highlight-overdue',
    install(ctx) {
      // tap returns a disposer; hand it back and the engine cleans up on destroy
      return ctx.hooks.rows.tap((rows) =>
        rows.map((row) => ({
          ...row,
          tasks: row.tasks.map((task) =>
            new Date(task.end) < new Date() && (task.progress ?? 0) < 1
              ? { ...task, className: 'task-overdue' }
              : task,
          ),
        })),
      )
    },
  }
}

Unit tests without a browser

The engine is pure logic. Construct it in Node, feed it rows, and assert on the typed output. No jsdom, no headless browser, no snapshot of rendered pixels.

const engine = new GanttEngine({ rows, viewMode: 'Week' })
const scene = engine.getScene()

expect(scene.layers.map((l) => l.name)).toContain('bars')
expect(engine.getRows()).toHaveLength(2)

Keep reading

JavaScript Gantt chart

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

Read more

Gantt chart component

Wrap the typed engine as a React, Vue, Svelte or Angular component.

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

Does the Gantt chart library ship TypeScript types?

Every @ganttkit package publishes ESM and CJS builds with type definitions included, so there is no separate types package to install and no version skew between code and types.

Can I add custom fields to a row and keep them typed?

Row carries an index signature for extra fields, so you can extend the interface with your own domain properties and read them from column formatters.

Are plugins type-safe?

A plugin is an object with a name and an install function that receives the engine context: store, events, commands, services, ui and hooks. Each registry is typed, and services can be consumed with a generic type parameter.

Can I test Gantt layout without a browser?

The engine is pure logic with no DOM access, so you can construct it in Node, feed it rows, and assert on the typed scene and layout output in a unit test.