Events
Wire the chart's callbacks into your app, and put an edit back when the save fails.
The chart hands you every committed edit through onTasksChange, and reports clicks and selection
through callbacks of their own. Wire them into your state to keep your app and the chart in sync.
Receive every edit
A drag, a keyboard edit and a detail panel field all commit the same way. The chart writes the
result into its own copy of the tasks, then calls onTasksChange once with the whole array.
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';
const initialTasks: Task[] = [
{ id: 'design', name: 'Design', parentId: null, sequence: '1',
startDate: '2026-03-02', endDate: '2026-03-06' },
{ id: 'build', name: 'Build', parentId: null, sequence: '2',
startDate: '2026-03-09', endDate: '2026-03-20' },
];
export function ProjectChart() {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
return (
<ReactGanttChart
tasks={tasks}
onTasksChange={setTasks}
showTaskList
height={420}
/>
);
}The array holds every task, not only the edited one, so dragging a summary bar delivers its whole subtree in that one call.
onTasksChange receives the array and nothing else, with no gesture type, grabbed edge or list of
moved ids. Diff it against the one you last passed in to find what changed.
A gesture that never crosses a drag step ends with no write and no callback.
Save to your server and revert
The chart applies the change before your handler runs, so the screen is already updated when you send the request. An optimistic update is the default.
<ReactGanttChart
tasks={tasks}
onTasksChange={(next) => {
const previous = tasks;
setTasks(next);
save(next).catch(() => setTasks(previous));
}}
/>The revert is a second setTasks with the array you held before. The tasks prop wins whenever
its contents differ from what the chart holds, so the bar returns to where it was.
The chart recognizes the array it just handed you when it comes back as tasks, so the ordinary
controlled loop leaves the drag result on screen.
Nothing is stored or sent for you. Persistence is your app's job.
React to clicks and selection
A click on a bar and a click on its task list row fire the same callback with the same argument,
the task plus its computed layout fields (Task types). Inside one click the order is
fixed. onTaskClick runs first, the selection changes next, and the
detail panel opens last when it is on.
Selection has no fixed default. Passing onTaskSelect turns it on, and so does turning the detail
panel on. selectable overrides both in either direction. With selectable={false} the selection
path exits before your handler, so onTaskSelect is never called.
onTaskSelect fires only when the selected id changes, so clicking the selected row fires
onTaskClick alone. Clicking the empty timeline clears the selection and fires
onTaskSelect(null); clicking there again fires nothing. The task list's empty area and the date
header leave the selection alone.
The chart drops the click that closes a bar drag, so a finished drag fires no onTaskClick. The
flag it checks is set only when the snapped step count changed, so a press that never moved is
still a click. Task list rows carry no such flag, and pressing the reorder grip fires
onTaskClick and selects the row either way. The collapse control stops its own click, so
expanding a row never selects it.
There is no click count debounce, so single click work runs twice before a double click opens your editor.
onDetailChange fires on every open and close, in controlled and uncontrolled mode alike, for
gestures and ref calls only. Setting detailTaskId yourself fires nothing, and a task removed
from tasks closes its panel without firing it.
Cancel a change before it lands
onDependencyCreate, onDependencyDelete and onTaskMove run before the change is applied.
Return literal false from any of them and the change is dropped, with nothing written and no
onTasksChange.
All three are synchronous. The chart reads the return value immediately, so a promise is truthy and the change commits; validate on the server in your save flow. The payloads are in Dependencies and Reordering rows.
Dates and progress have no callback of this kind. A rule that every edit must satisfy is enforced
after the fact, in onTasksChange.
Before you integrate
- Selection is one internal id. There is no multi-select and no
selectedTaskIdprop, so track it fromonTaskSelect. - Context menu, hover and focus have no callback, and right click is not reported. Add your own
listener and read
data-task-idoff the event target. - Drag lifecycle callbacks do not exist. There is no
onDragStart,onDragMoveoronDragEnd, andonTasksChangefires at the end of a gesture. - A bar drag the browser cancels, through a scroll takeover or a second finger, reverts and fires
nothing. A progress drag canceled the same way commits and fires
onTasksChange.
Callback summary
| Action | Callbacks, in order | Can cancel |
|---|---|---|
| A click on a bar or its row | onTaskClick, onTaskSelect, then onDetailChange when the panel is on | no |
| A double click on a bar or its row | onTaskClick twice, then onTaskDoubleClick | no |
| A click on the empty timeline | onTaskSelect(null), and onDetailChange(null) when the panel was open | no |
| A move, a resize, a progress drag, a keyboard edit or a panel field edit | onTasksChange | no |
| A row dragged to a new slot or parent | onTaskMove, then onTasksChange | yes |
| A dependency arrow drawn or deleted | onDependencyCreate or onDependencyDelete, then onTasksChange | yes |
| A task added from the button, the ref or a drawn range | onTaskCreate | the chart writes nothing |
| A parent row expanded or collapsed | onCollapsedChange | no |
| The detail panel opened or closed | onDetailChange | no |
| A change to the scale or the rendered range | onScaleChange, onRangeChange | no |
Props and defaults: GanttProps.