Layout and sizes
The size of everything the chart draws comes from the layout option, in pixels:
rowHeight: the height of a task’s row, the room for its bar and the space around it;barHeight: the height of a bar, centred in its row;barRadius: the roundness of its corners;headerRowHeight: the height of each row of the header (month, week, day, …);height: the height of the chart itself, which only the renderer knows.
The sliders change the first four with setOptions({ layout }), nothing is recreated, and the line
under the chart says what the options amount to. The select switches the chart between a
fixed height (the chart is that tall and scrolls inside itself) and 'auto' (the chart is as
tall as its rows, and the page scrolls it, with the header stuck to the top). The dashed box is
the page.
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';
// Ten tasks in March 2026: enough rows to be taller than the chart, bars of different lengths,// progress (the filled part of a bar follows its corners), and a milestone.export const tasks: Task[] = [ { id: 'discovery', name: 'Discovery', start: '2026-03-02', end: '2026-03-06', progress: 100 }, { id: 'design', name: 'Design', start: '2026-03-09', end: '2026-03-13', progress: 100 }, { id: 'prototype', name: 'Prototype', start: '2026-03-12', end: '2026-03-20', progress: 70 }, { id: 'backend', name: 'Backend', start: '2026-03-16', end: '2026-03-31', progress: 40 }, { id: 'frontend', name: 'Frontend', start: '2026-03-23', end: '2026-04-07', progress: 25 }, { id: 'docs', name: 'Docs', start: '2026-03-23', end: '2026-04-03', progress: 10 }, { id: 'marketing', name: 'Marketing', start: '2026-03-30', end: '2026-04-10' }, { id: 'qa', name: 'QA', start: '2026-04-08', end: '2026-04-14' }, { id: 'review', name: 'Review', start: '2026-04-15', end: '2026-04-16' }, { id: 'launch', name: 'Launch', type: 'milestone', start: '2026-04-20' },];
export const links: Link[] = [ { source: 'discovery', target: 'design' }, { source: 'design', target: 'prototype' }, { source: 'prototype', target: 'backend' }, { source: 'prototype', target: 'frontend' }, { source: 'backend', target: 'qa' }, { source: 'frontend', target: 'qa' }, { source: 'qa', target: 'review' }, { source: 'review', target: 'launch' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { viewMode: 'week', // Sizes in px. Each one can be changed later with `setOptions({ layout: { … } })`. layout: { // The height of a task's row: the room for its bar and the space around it (default 48). rowHeight: 48, // The height of a bar, centred in its row (default 30, or the row's height when that is less). // A bar cannot be taller than its row: a taller `barHeight` is clamped, with a warning. barHeight: 30, // The corner radius of a bar (default 3). Each bar limits it to half its own height and width. barRadius: 3, // The height of every header row (one number), or one number per row, top first (the default // is [45, 30]; a row with no number of its own is 30). headerRowHeight: 40, // The height of the chart itself, which only the renderer knows: a number of px, and the chart // scrolls inside that; 'auto', and the chart is as tall as its rows and the page scrolls it; // left out, and the chart fills its container. height: 300, }, // No timeline before the first task, so the chart opens at the left end of its timeline (the // default padding of the week view is '4w'). timeline: { padding: '0d' }, scroll: { initial: 'first-task' },} satisfies Partial<GanttOptions>;import '@ganttlane/dom/styles.css';
import { createGantt } from '@ganttlane/dom';
import { links, tasks } from './data';import { describe, FIXED_HEIGHT, type HeightKind, HEIGHTS, type Sizes, SLIDERS } from './helpers';import { options } from './options';
export function mount(container: HTMLElement): () => void { const root = document.createElement('div'); const toolbar = document.createElement('div'); toolbar.setAttribute('role', 'toolbar'); // The page: a box with a height of its own, which scrolls, as a page does. A chart with // `layout.height: 'auto'` is as tall as its rows and the page scrolls it. const page = document.createElement('div'); page.style.height = '360px'; page.style.overflow = 'auto'; page.style.border = '1px dashed currentColor'; // This container has no height: the chart is given one by `layout.height`. const chart = document.createElement('div'); page.append(chart); const status = document.createElement('p'); root.append(toolbar, page, status); container.append(root);
const gantt = createGantt(chart, { tasks, links, ...options });
const sizes: Sizes = { rowHeight: options.layout.rowHeight, barHeight: options.layout.barHeight, barRadius: options.layout.barRadius, headerRowHeight: options.layout.headerRowHeight, }; let height: HeightKind = 'fixed'; // `layout` merges field by field with the sizes set before. const apply = (): void => { gantt.setOptions({ layout: { ...sizes, height: height === 'auto' ? 'auto' : FIXED_HEIGHT } }); status.textContent = describe(sizes, height); }; status.textContent = describe(sizes, height);
const controller = new AbortController(); const { signal } = controller;
for (const { key, label, min, max, step } of SLIDERS) { const input = document.createElement('input'); input.type = 'range'; input.min = String(min); input.max = String(max); input.step = String(step); input.value = String(sizes[key]); input.style.width = '5.5rem'; const output = document.createElement('output'); output.textContent = input.value; input.addEventListener( 'input', () => { sizes[key] = Number(input.value); output.textContent = input.value; apply(); }, { signal }, ); const wrap = document.createElement('label'); wrap.append(`${label} `, input, ' ', output); toolbar.append(wrap); }
const select = document.createElement('select'); for (const kind of HEIGHTS) { const option = document.createElement('option'); option.value = kind; option.textContent = kind === 'auto' ? "'auto'" : String(FIXED_HEIGHT); select.append(option); } select.addEventListener( 'change', () => { const kind = HEIGHTS.find((candidate) => candidate === select.value); if (!kind) return; height = kind; apply(); }, { signal }, ); const selectLabel = document.createElement('label'); selectLabel.append('Height ', select); toolbar.append(selectLabel);
return () => { controller.abort(); gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';
import { Gantt } from '@ganttlane/react';import { useState } from 'react';
import { links, tasks } from './data';import { describe, FIXED_HEIGHT, type HeightKind, HEIGHTS, type Sizes, SLIDERS } from './helpers';import { options } from './options';
export default function Example() { const [sizes, setSizes] = useState<Sizes>({ rowHeight: options.layout.rowHeight, barHeight: options.layout.barHeight, barRadius: options.layout.barRadius, headerRowHeight: options.layout.headerRowHeight, }); const [height, setHeight] = useState<HeightKind>('fixed'); // In React `viewMode` is a controlled prop, and `defaultViewMode` is its uncontrolled twin: here // nothing changes the view mode, so the chart owns it and starts from the option. const { viewMode, ...rest } = options;
return ( <> <div role="toolbar"> {SLIDERS.map(({ key, label, min, max, step }) => ( <label key={key}> {label}{' '} <input type="range" min={min} max={max} step={step} value={sizes[key]} style={{ width: '5.5rem' }} onChange={(event) => { setSizes({ ...sizes, [key]: Number(event.target.value) }); }} />{' '} <output>{sizes[key]}</output> </label> ))} <label> Height{' '} <select value={height} onChange={(event) => { const kind = HEIGHTS.find((candidate) => candidate === event.target.value); if (kind) setHeight(kind); }} > {HEIGHTS.map((kind) => ( <option key={kind} value={kind}> {kind === 'auto' ? "'auto'" : FIXED_HEIGHT} </option> ))} </select> </label> </div> {/* The page: a box with a height of its own, which scrolls, as a page does. A chart with `layout.height: 'auto'` is as tall as its rows and the page scrolls it. */} <div style={{ height: 360, overflow: 'auto', border: '1px dashed currentColor' }}> <Gantt // This container has no height: the chart is given one by `layout.height`. // The chart owns the data (uncontrolled): these are where it starts. defaultTasks={tasks} defaultLinks={links} defaultViewMode={viewMode} {...rest} // `layout` is an ordinary prop: a changed field is applied with `setOptions`. layout={{ ...sizes, height: height === 'auto' ? 'auto' : FIXED_HEIGHT }} /> </div> <p>{describe(sizes, height)}</p> </> );}import { options } from './options';
/** The four sizes the sliders change. */export interface Sizes { rowHeight: number; barHeight: number; barRadius: number; headerRowHeight: number;}
/** The sliders: the option each one sets, its name and its range. */export const SLIDERS = [ { key: 'rowHeight', label: 'Row', min: 24, max: 80, step: 2 }, { key: 'barHeight', label: 'Bar', min: 10, max: 80, step: 2 }, { key: 'barRadius', label: 'Radius', min: 0, max: 20, step: 1 }, { key: 'headerRowHeight', label: 'Header rows', min: 20, max: 60, step: 2 },] as const satisfies readonly { key: keyof Sizes; label: string; min: number; max: number; step: number;}[];
/** The two ways to give the chart a height in this example. */export const HEIGHTS = ['fixed', 'auto'] as const;export type HeightKind = (typeof HEIGHTS)[number];
/** The height of the chart in the `'fixed'` setting, in px: the one in the options. */export const FIXED_HEIGHT = options.layout.height;
/** * Says which `layout` the controls amount to, and when the bar is clamped to its row. * * @param sizes - What the sliders hold. * @param height - What the height select holds. * @returns A sentence. */export function describe(sizes: Sizes, height: HeightKind): string { const { rowHeight, barHeight, barRadius, headerRowHeight } = sizes; const clamped = barHeight > rowHeight ? `. A bar cannot be taller than its row: ${String(barHeight)} px is drawn as ${String(rowHeight)} px` : ''; const shown = height === 'auto' ? "'auto'" : String(FIXED_HEIGHT); return `layout: { rowHeight: ${String(rowHeight)}, barHeight: ${String(barHeight)}, barRadius: ${String(barRadius)}, headerRowHeight: ${String(headerRowHeight)}, height: ${shown} }${clamped}`;}Files
data.ts: ten tasks in March and April, with progress and a milestone, and their links.options.ts:layout, each field commented, and the first scroll.helpers.ts: the sliders, anddescribe, which writes the status line.vanilla.ts: the sliders and the select,setOptions, and a box that stands for the page.react.tsx: the same with alayoutprop.
Options
| Option | Value | What it does |
|---|---|---|
layout.rowHeight |
48 |
The height of a row (the default): the slider Row |
layout.barHeight |
30 |
The height of a bar (the default), at most the row’s: Bar |
layout.barRadius |
3 |
The corner radius of a bar (the default), at most half its height and width: Radius |
layout.headerRowHeight |
40 |
One number for every header row, or a list, top first (default [45, 30]): Header rows |
layout.height |
300 |
Px: the chart is that tall and scrolls inside. 'auto': as tall as its rows. Left out: fills the container |
viewMode |
'week' |
The scale of the timeline (React: defaultViewMode, as nothing here changes it) |
timeline.padding |
'0d' |
No timeline before the first task (the week view has '4w'), so the chart opens at the plan |
scroll.initial |
'first-task' |
Opens on the plan |
What to try
Drag Row down to 30: the rows shrink, and so does the bar, which cannot be taller than its row
unless you set barHeight to say so. Then drag Bar up past the row’s height: the bars fill
their rows and no more, and the line under the chart says so (the option is clamped, and a
warning issue is reported). Drag Radius up: the corners round off until they are half the
bar’s height, a pill, and no further. Header rows sets all the header rows at once. Switch the
Height to 'auto' and the chart grows to show every row; scroll the dashed box (the page) and
the header stays at the top of it. Switch back, and the chart is 300 px again and scrolls itself.
Notes
- The defaults are a row of 48 px, a bar of 30 px (or the row’s height, when the row is shorter), corners of 3 px, and two header rows of 45 and 30 px. A header row past the second has 30 px.
- A list for
headerRowHeightgives each row of the header its own height, top first. Extra numbers are ignored and a missing one is the default, so one list suits view modes that have different numbers of header rows. A number sets them all. - A bad number (not finite, zero or negative where it has to be positive) is an
invalid-layout-optionissue and the default is used instead.setOptionsreturns the issues of the options now in use, andgetIssues().optionshas them too. layout.heightbelongs to the renderer, not to the core, so it is merged like the rest oflayoutbut never reaches the core. A number sets--gantt-heighton the root;'auto'setsdata-height="auto"and moves the header into a sticky strip, so that it sticks to the page or to whatever ancestor scrolls it. Left out, the chart takesheight: 100%of its container, which then needs a height of its own.- Anything else that is a size is a CSS custom property of the theme (
--gantt-font-size,--gantt-grid-line-width, …) that your stylesheet can set, see Release plan for a stylesheet of your own. - React.
layoutis an ordinary prop: the sliders change your state, a new object with a changed field is applied withsetOptionsfor you, andlayout.heightgoes through it too.viewModeis a controlled prop anddefaultViewModeits uncontrolled twin, so the example passes the latter.