Quick start
Install to a working, editable chart
You have an array of rows, each with a start date, an end date and a parent id. Turning that into
bars a user can drag is the part every app rewrites by hand. The sections below go from pnpm add
to a chart whose edits land in your own state. The first three code blocks are whole files; the
later ones are diffs against them.
Install
The package ships as ESM and CJS, with React as a peer dependency. React 18 and React 19 are both supported.
pnpm add @jaeungkim/gantt-chart
# npm install @jaeungkim/gantt-chart
# yarn add @jaeungkim/gantt-chartThen import the stylesheet once, anywhere in your app:
// src/main.tsx
import '@jaeungkim/gantt-chart/style.css';That specifier is a declared subpath export in package.json — "./style.css" maps to
./dist/gantt-chart.css — so it resolves without a deep path into dist. Skip the import and
nothing lays itself out. .gantt-container never gets its flex column, the bars keep the inline
translateX the component writes but not the position: absolute the stylesheet gives them, and
every --gantt-* token is undefined. The result reads as a stack of plain text rather than a
broken chart, so the missing import is easy to misread as a data problem.
Render three tasks
Six fields are required on every task: id, name, startDate, endDate, parentId and
sequence. sequence is a dot-separated string and it decides row order. A bare YYYY-MM-DD
date is read as UTC midnight, so it lands on the day it names in every viewer's time zone. The
Task data page owns those rules in full.
// src/tasks.ts
import type { Task } from '@jaeungkim/gantt-chart';
export const initialTasks: Task[] = [
{
id: 'design',
name: 'Design',
startDate: '2026-03-02',
endDate: '2026-03-06',
parentId: null,
sequence: '1',
progress: 100,
},
{
id: 'build',
name: 'Build',
startDate: '2026-03-09',
endDate: '2026-03-20',
parentId: null,
sequence: '2',
progress: 40,
dependencies: [{ targetId: 'design', type: 'FS' }],
},
{
id: 'ship',
name: 'Ship',
startDate: '2026-03-23',
endDate: '2026-03-23',
parentId: null,
sequence: '3',
type: 'milestone',
dependencies: [{ targetId: 'build', type: 'FS' }],
},
];dependencies lists what a task waits on, so build carries the link back to design. FS is
finish-to-start; the other three types are in Dependencies.
// src/ProjectChart.tsx
import { ReactGanttChart } from '@jaeungkim/gantt-chart';
import { initialTasks } from './tasks';
export function ProjectChart() {
return <ReactGanttChart tasks={initialTasks} height={420} />;
}height defaults to 600 and width to "100%". Both take a number of pixels or any CSS length
string. A percentage height only resolves if the parent element has a height of its own.
That chart is already editable with no further props. A bar can be dragged sideways to move the
task, and grabbed at either edge to resize it. design and build show a progress handle because
they carry a numeric progress; ship is a milestone diamond and never shows one. Hovering a bar
reveals a connector dot at each end; dragging from one onto another bar draws a dependency arrow.
Clicking an arrow selects it, and Delete or Backspace removes it. The toolbar's segmented control
switches between the six scales, hour to year. Ctrl/Cmd+Z undoes the last gesture and
Ctrl/Cmd+Shift+Z (or Ctrl/Cmd+Y) redoes it, while the focus is inside the chart.
None of it survives a reload. The chart keeps the edits in a store of its own, and that store is
created per instance and lives only as long as the component. The one thing that does persist is
the scale: the user's choice is written to sessionStorage under the key gantt-scale, which the
storageKey prop renames, so it comes back on remount within the same tab session.
Keep the edits
onTasksChange fires with the complete new task array after every committed gesture. Store it and
the chart becomes the editor for data you own.
// src/ProjectChart.tsx
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import { initialTasks } from './tasks';
export function ProjectChart() {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const handleTasksChange = (next: Task[]) => {
setTasks(next);
void fetch('/api/tasks', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(next),
});
};
return (
<ReactGanttChart
tasks={tasks}
onTasksChange={handleTasksChange}
height={420}
/>
);
}The callback hands back every task, not a patch, so the array can go straight into state or straight
into a request body. Dates come back as full ISO strings such as 2026-03-09T00:00:00.000Z, which
means the first drag changes the string form of a date you originally wrote as 2026-03-09.
[!WARNING] The
tasksprop is compared by content, not by array identity, so handing the same data back under a new array is deliberately ignored — see Task data for what that protects and when a genuine replacement does win.
Add the task list
The left pane is off until you ask for it. Passing columns turns it on by itself; passing
showTaskList on its own turns it on with the built-in Name / Start / End columns.
// src/ProjectChart.tsx
- import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
+ import {
+ ReactGanttChart,
+ type GanttColumn,
+ type Task,
+ } from '@jaeungkim/gantt-chart';
+
+ const columns: GanttColumn[] = [
+ { key: 'name', header: 'Task', width: 200 },
+ { key: 'startDate', header: 'Start', width: 110 },
+ { key: 'progress', header: '%', width: 60, render: (task) => task.progress ?? '' },
+ ]; <ReactGanttChart
tasks={tasks}
onTasksChange={handleTasksChange}
height={420}
+ columns={columns}
/>A column without a render prints task[key] as a string, and width defaults to 120 pixels. The
splitter between the two panes, the collapse button and the tree column are covered in
Task list and hierarchy.
Make it a tree
Row order always comes from sequence. Nesting is a separate opt-in: set hierarchy and depth is
read from the parentId chain instead.
// src/tasks.ts
+ {
+ id: 'phase-1',
+ name: 'Phase 1',
+ startDate: '2026-03-02',
+ endDate: '2026-03-20',
+ parentId: null,
+ sequence: '1',
+ },
{
id: 'design',
name: 'Design',
startDate: '2026-03-02',
endDate: '2026-03-06',
- parentId: null,
- sequence: '1',
+ parentId: 'phase-1',
+ sequence: '1.1',
progress: 100,
},Give build the same treatment: parentId: 'phase-1' and sequence: '1.2'. Leave every other
field on both tasks alone.
<ReactGanttChart
tasks={tasks}
onTasksChange={handleTasksChange}
height={420}
columns={columns}
+ hierarchy
/>phase-1 now has children, which makes it a summary row: its dates are recomputed from the
children rather than read from its own fields, and dragging its bar moves the whole subtree.
Task list and hierarchy explains the roll-up and the collapse state.
Limits
- The chart stores nothing. Without
onTasksChangean edit exists only in the component's own store, and a reload discards it. Fetching, saving and conflict resolution are the host app's job. - The chart never adds a task on its own. Drawing a range on empty row space exists only when you
pass
onTaskCreate, and it reports a draft rather than committing one; see Editing tasks. - It does remove one. Delete or Backspace on a focused row deletes that task and its whole subtree
and fires
onTasksChange; the same key press also removes a selected dependency arrow, so both can happen at once. A change in row count cannot be recorded as an undo step, so it clears the history instead. - There is no validation of task data. A
sequenceofundefinedthrows during render, and an unparseable date produces a bar spanning the whole timeline with no warning. Validate before passing the array in. - No fonts are bundled or fetched:
--gantt-font-sansis a system stack, and the host overrides it like any other token; see Theming. The only reset isbox-sizing: border-box, and it is scoped to.gantt-containerrather than exported globally. - The React component always renders its own toolbar and timeline. There is no unstyled mode of it. The scheduling, graph and calendar functions are separately importable without React, which is what Headless core is for.
Where to next
- The data does not look the way you expected: Task data.
- You need columns, indentation or collapsing: Task list and hierarchy.
- You need to lock down what can be dragged, or veto a change: Editing tasks.
- You want successors to move when a predecessor does: Scheduling.
- You want every prop in one table: Props.
Next: Task data, which owns the field-by-field contract this page only sketched.