Detail panel

Show one task's fields in a docked side panel and edit them in place.

Dragging a bar sets dates quickly but not precisely. Turn on the detail panel to show one task's fields beside the timeline, where people can read and type values instead.

Show the panel

Add showDetail. The panel docks to the right of the timeline, which narrows while it is open.

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-09-01', endDate: '2026-09-08', progress: 100 },
  { id: 'build', name: 'Build', parentId: null, sequence: '2',
    startDate: '2026-09-08', endDate: '2026-09-18', progress: 40,
    dependencies: [{ targetId: 'design', type: 'FS' }] },
];

export function ProjectChart() {
  const [tasks, setTasks] = useState(initialTasks);

  return (
    <ReactGanttChart
      tasks={tasks}
      onTasksChange={setTasks}
      showTaskList
      showDetail
      height={420}
    />
  );
}

The panel is off by default. showDetail renders the built-in body, and renderDetail turns the panel on by itself. An explicit showDetail={false} keeps it hidden either way.

A click on a bar or a task list row opens the panel, and so does Enter or Space on a focused row. On an expandable row those keys collapse or expand the row instead. Escape closes the panel from anywhere on the page without stopping the key, so your own handlers still run. Closing returns focus to where it was, unless focus has since moved.

Turning the panel on also turns row selection on, so the open task's row is highlighted; selectable={false} keeps the panel without it. onTaskClick fires before the panel opens. See Events.

The panel is not a dialog, so it has no focus trap or backdrop and the chart behind it stays interactive. See Keyboard and screen readers.

What the built-in panel shows

The body puts the task name above a short field list.

FieldValue
Start, EndThe dates, formatted by the current scale's tooltip format.
DurationThe span between them, in whole hours under a day and whole days above.
Progresstask.progress, clamped to 0 to 100.
Depends onThe names of the tasks its dependencies point at, or an id that names no task.

Progress and Depends on appear only when the task has them. No other field appears and no prop adds a row. The captions are fixed English; see Locale and date formats.

Edit fields in the panel

A field becomes an input where the task's flags already allow the matching gesture, because the panel adds no flag of its own. The name is a text input unless readOnly is set on the task or the chart.

Changing a date is a resize, so Start and End are date inputs when the task can be resized. A summary row's dates stay text.

Progress is a number input when its handle would be draggable. Duration and Depends on are derived and stay text. See interaction configuration for precedence.

A field commits on blur and on Enter. The change arrives through onTasksChange with the same whole array a drag sends, and the panel has no callback of its own. Escape reverts the field and leaves the panel open. A blank name, an incomplete date, an end that is not after the start and an emptied progress field revert with no message.

Editing a date replaces the date part and keeps the task's time of day, clamped to the same minDate and maxDate a drag obeys. A date input reads YYYY-MM-DD whatever the locale.

Render your own body

renderDetail replaces everything inside the panel. Its width, its accessible name and the focus handling on close stay the chart's. The close button is yours to render, and Escape still closes the panel.

<ReactGanttChart
  tasks={tasks}
  onTasksChange={setTasks}
  showTaskList
  renderDetail={({ task, close, update }) => (
    <>
      <h2>{task.name}</h2>
      <button type="button" onClick={() => update({ progress: 100 })}>
        Mark done
      </button>
      <button type="button" onClick={close}>
        Close
      </button>
    </>
  )}
/>

The renderer receives the open task as the chart transformed it, plus close, the current scale and update. update(patch) commits through the same path as every gesture and checks no interaction flags, so a custom body enforces its own rules. It is the chart's only render prop. See Detail renderer.

Control which task is open

Pass detailTaskId to hold the open task in your own state, with onDetailChange to receive every open and close. The chart is controlled as soon as detailTaskId is not undefined, so null means controlled and closed rather than turned off.

In controlled mode the chart opens nothing itself and waits for a new detailTaskId. Passing detailTaskId without handling onDetailChange makes every click look ignored.

onDetailChange fires in both modes, never on mount, and never when the open task's own data changes. Moving its bar does not fire it.

openDetail(taskId) and closeDetail() on the chart ref follow the same rule, do nothing while the panel is off, and ignore an unknown id. openDetail reaches collapsed rows too. See Imperative API.

Before you integrate

  • The open task leaving tasks closes the panel, and onDetailChange does not fire.
  • A detailTaskId naming no task leaves the panel closed with no error, and opens it when a resolvable id arrives.
  • Escape closes the panel and leaves the row selected. Clicking empty timeline space clears the selection as well.
  • --gantt-detail-width sets the width, and the timeline absorbs the change. There is no drag handle and no width prop. See Theming.
  • The panel shows one task at a time, docked on the right and opened by a single click. There is no double click mode, trigger prop, tab list or other placement.

Props and defaults: GanttProps.