A headless core emits a declarative scene of Vector primitives. Renderers paint it. Plugins shape it. Nothing is glued to a framework.
The engine outputs a pure data structure a list of Vector primitives. It never touches the DOM. Renderers map primitives to elements; plugins hook the data pipeline and the scene.
Viewport virtualization windows the scene to what's visible. A 20 000-task chart emits ~80 Vector nodes instead of ~90 000. Scroll rebuilds run under a millisecond two passes keep cost bounded regardless of dataset size.
Each plugin receives a context with store, events, commands, services, ui, and hooks. The sidebar, tree, toolbar, and i18n are all plugins nothing is hardwired into the engine.
The engine runs in Node no DOM, no browser globals required. Layout, time-scale, scene generation, and plugin behaviour are all unit-testable without a browser. 83 tests, all green.
Geometry is computed once in core; renderers map it to elements. Adding a React or Svelte renderer is a thin adapter, not a fork. All 11 feature plugins work across SVG, HTML, and Canvas unchanged.
Owns rows, time-scale, layout geometry, drag state, and the plugin host. Zero DOM, zero framework. Emits a declarative Scene through hooks and events.
Hook hooks.rows to transform data, or hooks.scene to add Vector layers. Publish capabilities via the service registry.
Subscribes to scene:change, maps primitives to Vector, forwards pointer events back to the engine. Hosts UI slots for plugin-contributed DOM.
Each plugin is a separate package with a single contract: { name, install(ctx) }. No hidden coupling, no global side effects.
Plain SVG display. No framework dependency. Works in any web environment.
Plain HTML/CSS. No framework dependency. Works in any web environment.
Canvas renderer with high performance drawing, suitable for large datasets.
Sidebar column definitions. Not in core renderers draw a sidebar only when installed.
Hierarchical rows with expand / collapse. Commands: toggle, expandAll, collapseAll.
Filter and sort rows via the data pipeline. Composable predicates, zero boilerplate.
Completion fill inside task bars, reading task.progress (0–1).
Vertical date markers and bands today line, deadlines, sprint ranges.
Finish-to-start auto-scheduling. Drag a task → dependents shift. Drag a connector → new link.
Resource↔task assignment scheduling. Drag-to-assign between resource lanes and tasks. Availability calendars with working days and blackout dates.
Planned-vs-actual ghost bars. baseline.capture() snapshots, baseline.clear() resets.
View-mode selector, zoom, jump-to-today. Contributed to the toolbar UI slot one package, any renderer.
Hover detail card with task name, dates, and completion. Locale-aware formatting.
Click-select, shift-drag rubber-band, and context menu. Custom actions, multi-select.
Locale-aware timeline + translatable strings. Runtime switching, no reload, subtag fallback.
Two-pass compute keeps cost bounded. The heavy pass runs on data change; the cheap pass runs on scroll. The renderer always sees a windowed scene not the full dataset.
recompute() runs the full pipeline on data change. rebuildScene(reason) runs a cheap windowed pass on scroll never re-invoking row hooks.
Only rows and day-columns inside the scroll viewport are emitted, plus a configurable overscan. Canvas stays full-size; only Vector children change.
Scroll events are debounced to the animation frame. Multiple fast scrolls merge into one rebuild no stale work queued.
// HTML + compose plugins, instantiate renderer import { createGantt } from '@ganttkit/html' import { createColumns } from '@ganttkit/plugin-columns' import { progressPlugin } from '@ganttkit/plugin-progress' import { createTree } from '@ganttkit/plugin-tree' import { toolbarPlugin } from '@ganttkit/plugin-toolbar' const rows = [ { id: '1', name: 'Design', start: '2026-07-01', end: '2026-07-15' }, { id: '2', name: 'Development', start: '2026-07-10', end: '2026-08-05' }, ] const plugins = [ progressPlugin(), toolbarPlugin(), createColumns({ columns: [{ key: 'name', label: 'Task' }] }).plugin, createTree().plugin, ] const gantt = createGantt({ target: '#gantt', rows, plugins, startDate: '2026-07-01', endDate: '2026-08-31', })
Compose your task rows and plugins, instantiate with createGantt, and point it at a DOM target. No config files, no framework lock-in.
The same plugin array works across @ganttkit/svg, @ganttkit/html, and @ganttkit/canvas just swap the import.