GanttRoot, GanttChart and useGanttBar
React only. <GanttRoot> makes a chart (it takes the options <Gantt> takes) and shares it with
everything below it; <GanttChart> is the element the chart is drawn into, wherever you put it.
The pieces of the page are components of their own, and none is handed the chart as a prop:
Toolbarreads the chart withuseGanttContext()and its state withuseGanttState(selector).TaskListtakes the chart of the root when it is given none.TaskPanelcallsuseGanttBar(id)for the selected task, and goes with its bar.
pnpm add @ganttlane/react react react-domnpm install @ganttlane/react react react-domyarn add @ganttlane/react react react-dombun add @ganttlane/react react react-domimport type { Link, Task } from '@ganttlane/core';
// 2026-03-02 is a Monday.export const tasks: Task[] = [ { id: 'brief', name: 'Brief', start: '2026-03-02', end: '2026-03-04', progress: 100 }, { id: 'design', name: 'Design', start: '2026-03-05', end: '2026-03-13', progress: 70 }, { id: 'backend', name: 'Backend', start: '2026-03-16', end: '2026-03-27', progress: 40 }, { id: 'frontend', name: 'Frontend', start: '2026-03-16', end: '2026-04-03', progress: 20 }, { id: 'docs', name: 'Docs', start: '2026-03-30', end: '2026-04-03' }, { id: 'qa', name: 'QA', start: '2026-04-06', end: '2026-04-10' }, { id: 'release', name: 'Release', type: 'milestone', start: '2026-04-13' },];
export const links: Link[] = [ { source: 'brief', target: 'design' }, { source: 'design', target: 'backend' }, { source: 'design', target: 'frontend' }, { source: 'backend', target: 'qa' }, { source: 'frontend', target: 'qa' }, { source: 'docs', target: 'qa' }, { source: 'qa', target: 'release' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { // The scale of the timeline. (`GanttRoot` takes it as `viewMode`, controlled, or as // `defaultViewMode`, which the chart then owns: see `react.tsx`.) viewMode: 'week', // Record every change, so that the toolbar's Undo and Redo have something to undo. history: true, // A click on a bar selects it: the panel follows the selected task. selection: { onClick: true, clearOnEmptyClick: true }, // Open on the plan. The default, 'today', is far from these dates. scroll: { initial: 'first-task' },} satisfies Partial<GanttOptions>;import '@ganttlane/react/styles.css';import './styles.css';
import { GanttChart, GanttRoot, TaskList, useGanttBar, useGanttContext, useGanttState,} from '@ganttlane/react';
import { links, tasks } from './data';import { options } from './options';
const VIEW_MODES = ['day', 'week', 'month'];const day = new Intl.DateTimeFormat('en', { day: 'numeric', month: 'short' });
/** * A toolbar. It has no props: the chart comes from the root through `useGanttContext`, and what * it shows from `useGanttState(selector)`, which also reads the root's chart. */function Toolbar() { const gantt = useGanttContext(); const mode = useGanttState((snapshot) => snapshot.viewMode.id); // History is read from the instance; it changes together with the snapshot. const canUndo = useGanttState(() => gantt.canUndo); const canRedo = useGanttState(() => gantt.canRedo);
return ( <div className="compound-toolbar" role="toolbar" aria-label="Chart controls"> <label> View{' '} <select value={mode} onChange={(event) => { gantt.setViewMode(event.target.value); }} > {VIEW_MODES.map((id) => ( <option key={id} value={id}> {id} </option> ))} </select> </label> <button type="button" disabled={!canUndo} onClick={() => { gantt.undo(); }} > Undo </button> <button type="button" disabled={!canRedo} onClick={() => { gantt.redo(); }} > Redo </button> </div> );}
/** * A panel that follows the selected task. `useGanttBar` gives the task, its draft (the dates and * progress the chart shows while the bar is dragged), and `box`: where the bar is drawn, in px * from the corner of the chart's element. The panel goes under the bar, and is hidden when the * bar is scrolled out of view (or under the header). */function TaskPanel() { const id = useGanttState((snapshot) => snapshot.selection.tasks[0]); const bar = useGanttBar(id); const width = useGanttState((snapshot) => snapshot.timeline.viewportWidth); const height = useGanttState((snapshot) => snapshot.viewportHeight); const header = useGanttState((snapshot) => snapshot.layout.rows.headerHeight);
if (bar?.box === undefined) return null; const { task, draft, box, isDragging } = bar; const inView = box.left + box.width > 0 && box.left < width && box.top + box.height > header && box.top < height; if (!inView) return null;
const start = draft?.start ?? task.start; const end = draft?.end ?? task.end; const progress = draft?.progress ?? task.progress; return ( <aside className="compound-panel gantt-scope" data-dragging={isDragging ? '' : undefined} style={{ left: Math.max(box.left, 0), top: box.top + box.height + 6 }} > <strong>{task.name}</strong> {start && end ? `${day.format(start)} to ${day.format(end)}` : 'No dates'} <br /> {Math.round(progress)}% done{isDragging ? ', being dragged' : ''} </aside> );}
export default function Example() { // `viewMode` is a controlled prop in React: the toolbar changes it through the chart, so the // chart starts from `defaultViewMode` and owns the mode. const { viewMode, ...rest } = options;
return ( <GanttRoot {...rest} defaultViewMode={viewMode} defaultTasks={tasks} defaultLinks={links}> <Toolbar /> <div className="compound-split"> <TaskList /> <div className="compound-chart"> <GanttChart className="compound-chart__gantt" /> <TaskPanel /> </div> </div> </GanttRoot> );}/* * The pieces of the composition, each a component of the example's own: * * .compound-toolbar the toolbar above the chart (it reads the chart through the context) * .compound-split the list and the chart side by side * .compound-chart the chart's column: `position: relative`, so that the panel is placed in it * .compound-panel the panel that follows the selected bar * * The panel is placed with `left` and `top` from `useGanttBar(...).box`, which are px from the * top left corner of the element the chart is drawn into, the header included. */
.compound-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-block-end: 8px;}
.compound-split { display: flex; height: 360px;}
.compound-chart { position: relative; flex: 1; min-width: 0;}
.compound-chart > .compound-chart__gantt { height: 100%;}
.compound-panel { position: absolute; z-index: 3; box-sizing: border-box; width: 200px; padding: 8px 10px; font: 12px/1.4 system-ui, sans-serif; color: var(--gantt-popover-foreground-color); pointer-events: none; background: var(--gantt-popover-background-color); border: 1px solid var(--gantt-popover-border-color); border-radius: 6px;}
.compound-panel[data-dragging] { border-style: dashed;}
.compound-panel strong { display: block;}Files
data.ts: seven tasks and the links between them.options.ts: the view mode,history, selection by click and the first scroll.react.tsx:Example(the root),ToolbarandTaskPanel.styles.css: the layout of the pieces, and the panel.
Options
| Option | Value | What it does |
|---|---|---|
defaultViewMode |
'week' |
The scale of the timeline; the chart owns it, so the toolbar can change it |
history |
true |
Records every change, so that Undo and Redo have something to say |
selection |
{ onClick: true, clearOnEmptyClick: true } |
A click on a bar selects the task, which is what the panel follows |
scroll.initial |
'first-task' |
Opens on the plan; the default, 'today', is far from these dates |
defaultTasks, defaultLinks |
the tasks and links | Where the chart starts with its data; it owns it from then on |
What to try
- Select a bar. The panel appears under it, with the task’s dates and progress.
- Drag the bar. The panel goes with it, and its dates change as the bar’s do: they are the
draftofuseGanttBar, the values the chart shows before the change is committed. The border turns dashed whileisDraggingis true. - Scroll the chart. The panel stays under the bar, and goes away when the bar is out of view.
boxis in px from the corner of the chart’s element, the scroll taken off. - Change the view mode, or drag a bar and undo. The toolbar is a component with no props.
Notes
GanttRootdraws no element. It renders its children and the portals of the slots, so what a slot draws sees the same context (renderBarLabelmay calluseGanttBartoo).- A context cannot know
task.data.GanttRootinfers it from its props; below it, name it:useGanttContext<MyData>(),useGanttBar<MyData>(id). useGanttBarfollows frame by frame.boxchanges while the chart scrolls and the bar is dragged, so the component that calls it renders then. Keep it in a component of its own, as here, and read the selection alone withuseGanttState.- One
GanttChartper root. A second one is reported in development. - Without a root.
useGanttBar(id, gantt)anduseGanttState(gantt, selector)take a chart fromuseGanttinstead, and<Gantt>is still the chart in one element.