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
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
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.