A release plan
A plan with the pieces most charts need: phases that hold children, children pinned to the edges of
their phase, finished work that is locked, links with a lead and a lag, a milestone, a work
calendar with holidays, striped groups, status colours from CSS, and a typed data payload that
reaches every handler and render function.
pnpm add @ganttlane/dom # vanillapnpm add @ganttlane/react react react-dom # Reactnpm install @ganttlane/dom # vanillanpm install @ganttlane/react react react-dom # Reactyarn add @ganttlane/dom # vanillayarn add @ganttlane/react react react-dom # Reactbun add @ganttlane/dom # vanillabun add @ganttlane/react react react-dom # Reactimport type { Link, Task } from '@ganttlane/core';
/** What `task.data` holds. It is the type of `record.task.data` in every event and render function. */export interface Meta { owner: string; status: 'done' | 'active' | 'late';}
export const tasks: Task<Meta>[] = [ // ---- Design: finished, so locked (no drag, and no propagation moves it) ---- { id: 'design', name: 'Design', start: '2026-03-02', end: '2026-03-13', progress: 100, locked: true, className: 'is-done', data: { owner: 'Ann', status: 'done' }, }, { id: 'draft', name: 'Draft', parentId: 'design', start: '2026-03-02', end: '2026-03-06', progress: 100, locked: true, className: 'is-done', data: { owner: 'Ann', status: 'done' }, }, { id: 'sign-off', name: 'Sign-off', parentId: 'design', start: '2026-03-09', end: '2026-03-13', progress: 100, locked: true, className: 'is-done', data: { owner: 'Ann', status: 'done' }, },
// ---- Build: counted in working days; its children follow it ---- { id: 'build', name: 'Build', start: '2026-03-16', end: '2026-04-10', durationMode: 'working', progress: 35, className: 'is-active', data: { owner: 'Bo', status: 'active' }, }, { id: 'backend', name: 'Backend', parentId: 'build', start: '2026-03-16', end: '2026-03-27', parentAnchor: 'SS', // its start is pinned to the start of Build followParent: true, // and it moves when Build moves (`hierarchy.moveChildren: 'following'`) progress: 50, className: 'is-active', data: { owner: 'Bo', status: 'active' }, }, { id: 'frontend', name: 'Frontend', parentId: 'build', start: '2026-03-23', end: '2026-04-10', parentAnchor: ['SS', 'FF'], // both edges pinned: it stretches with Build followParent: true, progress: 20, className: 'is-late', data: { owner: 'Cy', status: 'late' }, },
// ---- QA and the release ---- { id: 'qa', name: 'QA', start: '2026-04-10', // a day before Build ends duration: 5, // days data: { owner: 'Ann', status: 'active' }, }, { id: 'release', name: 'Release', type: 'milestone', // a diamond at its start start: '2026-04-17', data: { owner: 'Ann', status: 'active' }, },];
export const links: Link[] = [ { source: 'design', target: 'build' }, // finish-to-start; no lag: it keeps the gap it has now { source: 'build', target: 'qa', lag: -1 }, // a lead: QA starts a day before Build ends { source: 'qa', target: 'release', lag: '2d' }, // a lag: two days after QA ends];import type { GanttOptions } from '@ganttlane/dom';
import type { Meta } from './data';
export const options = { viewMode: 'week', scroll: { initial: 'first-task' },
// Mondays to Fridays, with two named holidays that also stripe the grid. calendar: { holidays: [ { date: '2026-04-03', name: 'Holiday' }, { date: '2026-04-06', name: 'Office closed' }, ], }, hierarchy: { // Moving a phase moves the children that follow it (`followParent` on the task). moveChildren: 'following', // Design starts collapsed: only Build is expanded. expandedKeys: ['build'], }, display: { rowStripes: 'group', nonWorkingDays: 'stripes' },
tooltip: { // `record.task.data` is the typed `Meta` payload of the task. render: ({ task: record }) => ({ title: record.name, subtitle: `${record.task.data?.owner ?? 'Unassigned'} · ${record.task.data?.status ?? ''}`, }), }, onBeforeTaskChange: ({ conflicts }) => { // Conflicts are warnings; here a bar that would leave its phase is not allowed. return !conflicts.some((conflict) => conflict.type === 'outside-parent'); },} satisfies Partial<GanttOptions<Meta>>;import '@ganttlane/dom/styles.css';import './styles.css';
import { createGantt } from '@ganttlane/dom';
import { links, type Meta, tasks } from './data';import { options } from './options';
export function mount(container: HTMLElement): () => void { const root = document.createElement('div'); const toolbar = document.createElement('div'); toolbar.setAttribute('role', 'toolbar'); const chart = document.createElement('div'); chart.style.height = '420px'; root.append(toolbar, chart); container.append(root);
// `Meta` is the type of `task.data`: it flows to every handler and render function. const gantt = createGantt<Meta>(chart, { tasks, links, ...options });
const controller = new AbortController(); const actions = [ ['Expand all', () => gantt.expandAll()], ['Collapse all', () => gantt.collapseAll()], ] as const; for (const [label, run] of actions) { const button = document.createElement('button'); button.type = 'button'; button.textContent = label; button.addEventListener('click', run, { signal: controller.signal }); toolbar.append(button); }
return () => { controller.abort(); gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';import './styles.css';
import { Gantt, type GanttHandle } from '@ganttlane/react';import { useRef } from 'react';
import { links, type Meta, tasks } from './data';import { options } from './options';
export default function Example() { // `Meta` is the type of `task.data`: the handle's methods and every render function know it. const chart = useRef<GanttHandle<Meta>>(null); // The expanded parents are a prop of their own in React (`defaultExpandedKeys`: the chart owns // them from then on, so the buttons below can change them). const { hierarchy, ...rest } = options;
return ( <> <div role="toolbar"> <button type="button" onClick={() => chart.current?.expandAll()}> Expand all </button> <button type="button" onClick={() => chart.current?.collapseAll()}> Collapse all </button> </div> <Gantt<Meta> ref={chart} style={{ height: 420 }} defaultTasks={tasks} defaultLinks={links} {...rest} hierarchy={{ moveChildren: hierarchy.moveChildren }} defaultExpandedKeys={hierarchy.expandedKeys} /> </> );}/* * Your own stylesheet: any stylesheet of the page, loaded after the library's. Status colours are * one custom property per class. `task.className` is added to the bar's own element, so a CSS * variable set on it reaches the bar and its progress. */.gantt__bar.is-done { --gantt-bar-color: #2f9e44;}.gantt__bar.is-active { --gantt-bar-color: #3f6fd1;}.gantt__bar.is-late { --gantt-bar-color: #d9480f;}Files
data.ts: the tasks (aTask<Meta>carries your owndata), and the links.options.ts: the calendar, the hierarchy,display, the tooltip and a before-hook.styles.css: your own stylesheet, one custom property per status class.vanilla.tsandreact.tsx:createGantt<Meta>and<Gantt<Meta>>, with buttons that expand and collapse the phases.
Options
| Option | Value | What it does |
|---|---|---|
calendar.holidays |
two named days | Days off; they stripe the grid and name themselves on hover |
hierarchy.moveChildren |
'following' |
Moving a phase moves the children that follow it |
hierarchy.expandedKeys |
['build'] |
Which phases are open (React: defaultExpandedKeys); Design is shut |
display.rowStripes |
'group' |
Zebra stripes per phase with its children |
display.nonWorkingDays |
'stripes' |
Days off are striped |
tooltip.render |
a function | Reads the typed task.data (owner, status) |
onBeforeTaskChange |
a function | Refuses a bar that would leave its phase |
task.className |
'is-done', 'is-active', 'is-late' |
Status colours, set in styles.css |
What to try
Drag Build: Backend and Frontend follow it, and QA follows along the link. Design is locked: it does not move, and nothing pushes it. Hover a bar for its owner and status, and a striped day for its name. The before-hook refuses any change that would put a child outside its phase.
Notes
- Phases and children.
parentIdmakes the tree and a pre-order list of rows. A parent with children gets a collapsed or expanded state fromhierarchy.expandedKeys(expand('design'),collapseAll()change it later). - Pinned children.
parentAnchorpins a child’s edges to its parent’s:'SS'the start to the start,'FF'the end to the end,'FS'the start to the parent’s end,'SF'the end to the parent’s start. Moving Build moves Backend; stretching Build stretches Frontend, which is pinned at both edges. - Locked work.
locked: trueremoves a task from the user’s reach and from propagation, but you can still link to it. - Lags and leads. A number is days (negative for a lead), a string a duration (
'3d','4h'). A link without alagkeeps the gap it has now. - Conflicts (a child outside its phase, a bar on a day off, a link whose gap is below its
minimum) are reported on the record (
record.conflicts), drawn on the bar, and passed toonBeforeTaskChange. - A milestone is a task of
type: 'milestone'with just astart. - Your own stylesheet is any stylesheet of the page, loaded after the library’s.
task.classNameis added to the bar’s own element, so a custom property set on it reaches the bar and its progress.