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.

ProjectChart.tsx
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 selectedTaskId prop, so track it from onTaskSelect.
  • Context menu, hover and focus have no callback, and right click is not reported. Add your own listener and read data-task-id off the event target.
  • Drag lifecycle callbacks do not exist. There is no onDragStart, onDragMove or onDragEnd, and onTasksChange fires 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

ActionCallbacks, in orderCan cancel
A click on a bar or its rowonTaskClick, onTaskSelect, then onDetailChange when the panel is onno
A double click on a bar or its rowonTaskClick twice, then onTaskDoubleClickno
A click on the empty timelineonTaskSelect(null), and onDetailChange(null) when the panel was openno
A move, a resize, a progress drag, a keyboard edit or a panel field editonTasksChangeno
A row dragged to a new slot or parentonTaskMove, then onTasksChangeyes
A dependency arrow drawn or deletedonDependencyCreate or onDependencyDelete, then onTasksChangeyes
A task added from the button, the ref or a drawn rangeonTaskCreatethe chart writes nothing
A parent row expanded or collapsedonCollapsedChangeno
The detail panel opened or closedonDetailChangeno
A change to the scale or the rendered rangeonScaleChange, onRangeChangeno

Props and defaults: GanttProps.