Reordering rows

Let people drag task-list rows to a new position or under a new parent.

A plan changes shape as often as it changes dates. Turn on row dragging so people can move a task-list row to a new position, or under a new parent.

Turn on row dragging

Row dragging is off by default, unlike the bar gestures. Set allowReorder on the chart to enable every row, and add hierarchy for moves that change a parent.

PlanBoard.tsx
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';

export function PlanBoard({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState(initialTasks);

  return (
    <ReactGanttChart
      tasks={tasks}
      onTasksChange={setTasks}
      showTaskList
      hierarchy
      allowReorder
    />
  );
}

A task's own allowReorder wins over the chart's, and readOnly: true on a task blocks its move.

The chart's readOnly is not consulted, because reordering has no permissive default to fall back to. See interaction configuration for the precedence.

A draggable row shows a grip on its left edge, on hover and on keyboard focus. A row that cannot move keeps a blank spacer there, so its text stays aligned.

A drag starts on the grip only. A press anywhere else in the row selects it, and Escape returns a dragged row to its place.

On touch, rest a finger on the grip for the same 400 ms a bar drag needs. See Editing tasks.

Alt with the vertical arrows moves a row among its siblings, and Ctrl/Cmd with the horizontal arrows indents or outdents it. See keyboard controls.

Where a row lands

The drop position depends on where in the target row you release.

RegionWhere the row lands
Top quarterBefore that row, as its sibling
Middle halfInside that row, as its last child
Bottom quarterAfter that row, as its sibling

The middle region is the widest, so changing a parent is the easiest target. A sibling region on a top-level row returns a nested row to the top level.

A drop into a collapsed row expands it.

Three drops are refused while the pointer is still down, so they never reach your app. A row cannot land inside its own subtree, a parent cannot change while hierarchy is off, and a drop that changes nothing is ignored.

What a move rewrites

sequence alone sets row order, and it is a path, so "2.1" is the first child of "2". No string sorts between "1.1" and "1.2", so a move renumbers instead.

One move rewrites sequence on every task whose path changed, and parentId on the moved task only. Children keep pointing at their parent and travel with it.

Tasks whose path did not change keep their object identity, so an app that diffs the array sees only what moved. The result arrives as a single onTasksChange carrying the whole array.

Dates are untouched. Nothing is stretched or clipped to fit a new parent, and links are stored by id, so arrows join the same two tasks wherever the row lands.

Bars keep their position, except on summary rows, whose spans are recomputed from their children. See Task list and hierarchy.

Approve or reject a move

onTaskMove runs before the move is applied. Return false to drop it. Nothing is written, onTasksChange is skipped, and the row stays put.

This example allows reordering inside a phase but rejects a change of parent.

<ReactGanttChart
  tasks={tasks}
  onTasksChange={setTasks}
  onTaskMove={({ fromParentId, toParentId }) => {
    if (fromParentId !== toParentId) return false;
  }}
  allowReorder
  hierarchy
/>

The change names the task and both ends of the move. fromParentId and fromIndex give the old position, toParentId and toIndex the new one.

afterId and beforeId name the siblings on either side after the move. The index pair suits an index-based API and the sibling pair a position-based one.

Both come from one result, so they cannot disagree. See tree helpers for the full shape.

onTaskMove is not awaited, so only a literal false rejects a move. A handler returning a promise has already allowed it.

To undo a move your server refuses, pass the previous tasks array back. See Events.

That works only while your app holds the array in state. A chart given a literal array tracks its own copy and has nothing to revert to.

Before you integrate

  • A row carrying more than one task can neither be dragged nor receive a drop, because a shared lane row names no single task to move.
  • A drop into a row appends last among its children. No region names a slot between two children, so drop the row as a sibling of that child instead.
  • A row drag does not auto-scroll. autoScrollOnDrag covers bar moves and resizes only, so scroll a distant drop target into view first.

Props and defaults: GanttProps.