# Data table: Row reorder

> Let people drag rows of the slotsmith data table into a new order by pointer, touch or keyboard, in flat and tree tables, and store the order.

The data table lets people drag rows into a new order, by pointer, touch or keyboard. Pass `enableRowReorder` and `onRowOrderChange`: the table adds a drag handle to each row, reports every drop, and changes nothing until you store the new `data`.

```tsx
import { useState, type CSSProperties } from "react";
import { DataTable, type DataTableColumnDef, type RowOrderChange } from "slotsmith/data-table";

type Step = { id: string; title: string; owner: string; estimate: string };

const plan: Step[] = [
  { id: "s1", title: "Write the release notes", owner: "Lena Park", estimate: "2h" },
  { id: "s2", title: "Freeze the main branch", owner: "Yuki Tanaka", estimate: "15m" },
  { id: "s3", title: "Run the full test suite", owner: "Diego Alvarez", estimate: "1h" },
  { id: "s4", title: "Tag the version", owner: "Yuki Tanaka", estimate: "5m" },
  { id: "s5", title: "Publish the package", owner: "Sofia Reyes", estimate: "10m" },
  { id: "s6", title: "Announce the release", owner: "Omar Haddad", estimate: "30m" },
];

// Sorting is off: this table is ordered by hand. (A drop while sorted would
// keep the order on screen and clear the sort.)
const columns: DataTableColumnDef<Step>[] = [
  { accessorKey: "title", header: "Step", enableSorting: false },
  { accessorKey: "owner", header: "Owner", enableSorting: false },
  { accessorKey: "estimate", header: "Estimate", enableSorting: false },
];

// The page's own text colour, so the button reads in light and dark themes.
const resetStyle: CSSProperties = { font: "inherit", color: "inherit", background: "none", border: "1px solid", borderRadius: 4, padding: "0.125rem 0.625rem" };

export default function RowReorder() {
  // The table does not reorder itself: the order lives here.
  const [rows, setRows] = useState(plan);
  const [change, setChange] = useState<RowOrderChange<Step> | null>(null);

  return (
    <>
      <DataTable
        data={rows}
        columns={columns}
        getRowId={(step) => step.id}
        enableRowReorder
        onRowOrderChange={(next) => {
          // Store the new order in the same event; save it to a server after.
          setRows(next.data);
          setChange(next);
        }}
        enablePagination={false}
      />
      <p>
        <button
          type="button"
          style={resetStyle}
          onClick={() => {
            setRows(plan);
            setChange(null);
          }}
        >
          Reset
        </button>{" "}
        <span dir="ltr">
          {change
            ? `Last move: { rowId: "${change.rowId}", targetId: "${change.targetId}", position: "${change.position}" }`
            : "Drag a handle, or focus one and press Space."}
        </span>
      </p>
    </>
  );
}
```

Drag a row by its handle, or focus a handle and press Space, an arrow key, then Space again. The line under the table shows the fields of the last move; Reset puts the rows back.

## Storing the order

The order is yours: `onRowOrderChange` receives a `RowOrderChange` and the table shows the new order only once `data` changes. Store it in the same event, and save it to a server afterwards; a table that waits for the reply shows the row slide back, then jump.

- `row` and `rowId`: the row that was dragged; `rows` and `rowIds`: every row that moved, in their new order (see [several rows at once](https://slotsmith.dev/components/data-table/guides/row-reorder/#several-rows-at-once)).
- `target` and `targetId`: the row it was dropped next to, always one of its siblings.
- `position`: `"before"` or `"after"` the target.
- `data`: the top-level rows in their new order.
- `parentId`, `parent` and `siblings`: for a row in a [tree](https://slotsmith.dev/components/data-table/guides/tree-rows/), its parent and the parent's children in their new order. For a top-level row, `parentId` and `parent` are `null` and `siblings` holds the same rows as `data`.

Give rows a stable id with [`getRowId`](https://slotsmith.dev/components/data-table/api/#props); `rowId`, `targetId` and the drag state use it. The library also exports `moveItem(items, from, to, position)`, the same move on a plain list.

## Tree tables

In a tree table a row moves among its siblings, and takes its expanded sub-rows with it; to move rows to other parents too, see [moving across parents](https://slotsmith.dev/components/data-table/guides/row-reorder/#moving-across-parents). For a sub-row, `data` is the unchanged top level: store `siblings` as the children of `parent`, as `placeSiblings` does here.

```tsx
import { useState, type CSSProperties } from "react";
import { DataTable, type DataTableColumnDef, type RowOrderChange } from "slotsmith/data-table";

type Task = { id: string; title: string; owner: string; parts?: Task[] };

const project: Task[] = [
  {
    id: "t1",
    title: "Design",
    owner: "Omar Haddad",
    parts: [
      { id: "t1.1", title: "Sketch the flows", owner: "Priya Nair" },
      { id: "t1.2", title: "Review with support", owner: "Jonas Weber" },
      { id: "t1.3", title: "Final mockups", owner: "Omar Haddad" },
    ],
  },
  {
    id: "t2",
    title: "Build",
    owner: "Lena Park",
    parts: [
      { id: "t2.1", title: "Database changes", owner: "Diego Alvarez" },
      { id: "t2.2", title: "API endpoints", owner: "Lena Park" },
    ],
  },
  { id: "t3", title: "Launch", owner: "Hana Kim" },
];

// Sorting is off: this table is ordered by hand.
const columns: DataTableColumnDef<Task>[] = [
  { accessorKey: "title", header: "Task", enableSorting: false },
  { accessorKey: "owner", header: "Owner", enableSorting: false },
];

/** Gives the moved row's parent its children in their new order; the rest of the tree is unchanged. */
function placeSiblings(tasks: Task[], change: RowOrderChange<Task>): Task[] {
  return tasks.map((task) =>
    task.id === change.parentId
      ? { ...task, parts: change.siblings }
      : task.parts
        ? { ...task, parts: placeSiblings(task.parts, change) }
        : task,
  );
}

// The page's own text colour, so the button reads in light and dark themes.
const resetStyle: CSSProperties = { font: "inherit", color: "inherit", background: "none", border: "1px solid", borderRadius: 4, padding: "0.125rem 0.625rem" };

export default function RowReorderTree() {
  const [rows, setRows] = useState(project);
  const [change, setChange] = useState<RowOrderChange<Task> | null>(null);

  return (
    <>
      <DataTable
        data={rows}
        columns={columns}
        getRowId={(task) => task.id}
        getSubRows={(task) => task.parts}
        defaultExpanded={{ t1: true }}
        enableRowReorder
        onRowOrderChange={(next) => {
          // A top-level row: `data` is the new order. A sub-row: `data` is
          // unchanged, and `siblings` are its parent's new children.
          setRows((current) => (next.parentId === null ? next.data : placeSiblings(current, next)));
          setChange(next);
        }}
        enablePagination={false}
      />
      <p>
        <button
          type="button"
          style={resetStyle}
          onClick={() => {
            setRows(project);
            setChange(null);
          }}
        >
          Reset
        </button>{" "}
        <span dir="ltr">
          {change
            ? `Last move: { rowId: "${change.rowId}", targetId: "${change.targetId}", parentId: ${change.parentId === null ? "null" : `"${change.parentId}"`} }`
            : "Open sub-rows move with their row."}
        </span>
      </p>
    </>
  );
}
```

Drag Design below Build: its three open sub-rows travel with it. Drag a sub-row of Design: it stays inside Design. A row with no visible sibling, such as the only open child of a parent, has its handle disabled.

## Several rows at once

With [row selection](https://slotsmith.dev/components/data-table/guides/sorting-and-selection/) on, dragging a selected row drags every selected sibling with it. The selected rows close up under the dragged one as it is lifted, the other rows make room, and the group lands together, in its own order, at the gap it is dropped in; a scattered selection dropped where it started is put together there. `rows` and `rowIds` list the rows that moved, and `data` and `siblings` hold the new order as usual. Dragging a row that is not selected moves it alone. A group moves among the dragged row's siblings, and only the selected rows that are shown move with it.

## Between tables

Tables that share a [`reorderGroup`](https://slotsmith.dev/components/data-table/api/#props) exchange rows: drag a row out of one table and drop it in another. While the pointer is over the other table, a copy of the row follows it and a line marks where it would land. From the keyboard, a lifted row moves into the next table past its own table's last row, and into the previous one before its first. A selected group moves together.

```tsx
import { useState, type CSSProperties } from "react";
import { DataTable, type DataTableColumnDef, type RowTransfer } from "slotsmith/data-table";

type Card = { id: string; title: string; owner: string };

const backlog: Card[] = [
  { id: "c1", title: "Write the release notes", owner: "Lena Park" },
  { id: "c2", title: "Freeze the main branch", owner: "Yuki Tanaka" },
  { id: "c3", title: "Run the full test suite", owner: "Diego Alvarez" },
];

const doing: Card[] = [{ id: "c4", title: "Tag the version", owner: "Yuki Tanaka" }];

// Sorting is off: both tables are ordered by hand.
const columns: DataTableColumnDef<Card>[] = [
  { accessorKey: "title", header: "Card", enableSorting: false },
  { accessorKey: "owner", header: "Owner", enableSorting: false },
];

// The page's own text colour, so the button reads in light and dark themes.
const resetStyle: CSSProperties = { font: "inherit", color: "inherit", background: "none", border: "1px solid", borderRadius: 4, padding: "0.125rem 0.625rem" };

export default function RowReorderBetween() {
  const [todo, setTodo] = useState(backlog);
  const [inProgress, setInProgress] = useState(doing);
  const [last, setLast] = useState<RowTransfer<Card> | null>(null);

  return (
    <>
      <DataTable
        aria-label="Backlog"
        data={todo}
        columns={columns}
        getRowId={(card) => card.id}
        enableRowReorder
        // Tables with the same reorderGroup exchange rows.
        reorderGroup="board"
        reorderTableId="backlog"
        onRowOrderChange={(change) => setTodo(change.data)}
        // Called on both tables; `data` is always this table's new rows.
        onRowTransfer={(transfer) => {
          setTodo(transfer.data);
          setLast(transfer);
        }}
        enablePagination={false}
      />
      <DataTable
        aria-label="In progress"
        data={inProgress}
        columns={columns}
        getRowId={(card) => card.id}
        enableRowReorder
        reorderGroup="board"
        reorderTableId="in-progress"
        onRowOrderChange={(change) => setInProgress(change.data)}
        onRowTransfer={(transfer) => setInProgress(transfer.data)}
        enablePagination={false}
      />
      <p>
        <button
          type="button"
          style={resetStyle}
          onClick={() => {
            setTodo(backlog);
            setInProgress(doing);
            setLast(null);
          }}
        >
          Reset
        </button>{" "}
        <span dir="ltr">
          {last
            ? `Last transfer: ${last.rowIds.join(", ")} from "${last.from.tableId}" to "${last.to.tableId}" at ${last.to.index}`
            : "Drag a card into the other table, or lift one with Space and press ↓ past the last row."}
        </span>
      </p>
    </>
  );
}
```

Each table needs `enableRowReorder`, and `reorderTableId` names it in transfers. A drop calls [`onRowTransfer`](https://slotsmith.dev/components/data-table/api/#props) on both tables with the same `RowTransfer`: `rows` and `rowIds` moved, `from` is the table they left with its rows without them, `to` the table they joined with its rows with them, the `index` they landed at and the `target` they were dropped next to. `data` is always the receiving table's own new rows, so each table stores `transfer.data`. The receiving table's `canDrop` sees `fromTableId` and can refuse rows from other tables. A table's only row can be dragged out, and an empty table takes rows. Only top-level rows move between tables.

## Moving across parents

With [`reorderAcrossParents`](https://slotsmith.dev/components/data-table/api/#props), a row of a tree can move to another parent and another depth. Drag it up or down to pick the gap between two rows, and sideways to pick the depth: one indent to the right makes it a child of the row above, one to the left takes it out a level. From the keyboard, the left and right arrow keys do the same while a row is lifted. The rows make room as before, and the lifted row is drawn at its new depth.

```tsx
import { useState, type CSSProperties } from "react";
import { DataTable, type DataTableColumnDef, type RowOrderChange } from "slotsmith/data-table";

type Task = { id: string; title: string; owner: string; parts?: Task[] };

const project: Task[] = [
  {
    id: "t1",
    title: "Design",
    owner: "Omar Haddad",
    parts: [
      { id: "t1.1", title: "Sketch the flows", owner: "Priya Nair" },
      { id: "t1.2", title: "Review with support", owner: "Jonas Weber" },
    ],
  },
  {
    id: "t2",
    title: "Build",
    owner: "Lena Park",
    parts: [
      { id: "t2.1", title: "Database changes", owner: "Diego Alvarez" },
      { id: "t2.2", title: "API endpoints", owner: "Lena Park" },
    ],
  },
  { id: "t3", title: "Launch", owner: "Hana Kim" },
];

// Sorting is off: this table is ordered by hand.
const columns: DataTableColumnDef<Task>[] = [
  { accessorKey: "title", header: "Task", enableSorting: false },
  { accessorKey: "owner", header: "Owner", enableSorting: false },
];

// The page's own text colour, so the button reads in light and dark themes.
const resetStyle: CSSProperties = { font: "inherit", color: "inherit", background: "none", border: "1px solid", borderRadius: 4, padding: "0.125rem 0.625rem" };

export default function RowReorderAcross() {
  const [rows, setRows] = useState(project);
  const [change, setChange] = useState<RowOrderChange<Task> | null>(null);

  return (
    <>
      <DataTable
        data={rows}
        columns={columns}
        getRowId={(task) => task.id}
        getSubRows={(task) => task.parts}
        // How the table writes new children into a copy of a row, to build the new tree.
        setSubRows={(task, parts) => ({ ...task, parts })}
        defaultExpanded
        enableRowReorder
        reorderAcrossParents
        // Launch stays a top-level task.
        canDrop={(move) => move.rowId !== "t3" || move.parentId === null}
        onRowOrderChange={(next) => {
          // Across parents, `data` is the whole new tree.
          setRows(next.data);
          setChange(next);
        }}
        enablePagination={false}
      />
      <p>
        <button
          type="button"
          style={resetStyle}
          onClick={() => {
            setRows(project);
            setChange(null);
          }}
        >
          Reset
        </button>{" "}
        <span dir="ltr">
          {change
            ? `Last move: { rowId: "${change.rowId}", fromParentId: ${JSON.stringify(change.fromParentId)}, parentId: ${JSON.stringify(change.parentId)}, index: ${change.index} }`
            : "Drag a task sideways to move it in or out a level."}
        </span>
      </p>
    </>
  );
}
```

Drag API endpoints under Design, or drag Sketch the flows out to the top level. Launch only moves among the top-level tasks: `canDrop` refuses every other place for it.

The table builds the new tree itself, so it needs [`setSubRows`](https://slotsmith.dev/components/data-table/api/#props), which writes new children into a copy of a row; `getSubRows` reads them. A drop then reports the whole new tree as `data`: store it. `fromParentId` and `fromParent` say where the row came from, `parentId`, `parent`, `index` and `siblings` where it went, and `position` is `"inside"` when it became a child of the target (its first shown child, or its last when the target is collapsed). [`canDrop`](https://slotsmith.dev/components/data-table/api/#props) receives every place before it is offered (the row, the new `parent` and `parentId`, and the `index`), and the places it refuses are skipped. A row is never dropped inside its own sub-rows. Each step announces the level too, through the [`reorderLevel`](https://slotsmith.dev/components/data-table/api/#label-reorderLevel) label. The library also exports `moveInTree`, the same move on your own nodes.

## Keyboard

Each handle is a `<button>`, so it is in the tab order. With focus on a handle:

| Keys | Action |
| --- | --- |
| Space, Enter | Lift the row. |
| ↑, ↓ | Move the lifted row up or down one place among its siblings (across parents: one gap). |
| ←, → | With reorderAcrossParents, move the lifted row one level out or in. |
| Space, Enter | Drop the row in its new place. Focus stays on its handle. |
| Esc | Cancel. The row goes back where it was. |
| Tab | Cancel, and move focus on as usual. Leaving the handle any other way cancels too. |

While a row is lifted from the keyboard its handle has `aria-pressed="true"`, and the rows scroll so the target stays in view.

## Pointer and touch

The row follows the pointer and the rows it passes slide out of its way. Near the top or bottom edge of the scroll area, or of the window, the view scrolls. Letting go above, below or beside the row's siblings cancels the drag. The handle sets `touch-action: none`, so a touch on it drags the row instead of scrolling the page, and a press on it never counts as a click on the row (`onRowClick`).

## Announcements and labels

A visually hidden live region (`role="status"`, `aria-live="assertive"`) reads each step, with positions counted among the row's siblings: "Row lifted. Position 1 of 6.", "Position 2 of 6.", "Row dropped at position 2 of 6." or "Reordering cancelled.". Every handle's `aria-describedby` points at hidden instructions.

The six strings are labels, so a [locale pack](https://slotsmith.dev/components/data-table/api/#labels) translates them and `labels` replaces any of them: [`reorderRow`](https://slotsmith.dev/components/data-table/api/#label-reorderRow) names each handle, [`reorderInstructions`](https://slotsmith.dev/components/data-table/api/#label-reorderInstructions) is the instructions, and [`reorderLifted`](https://slotsmith.dev/components/data-table/api/#label-reorderLifted), [`reorderMoved`](https://slotsmith.dev/components/data-table/api/#label-reorderMoved), [`reorderDropped`](https://slotsmith.dev/components/data-table/api/#label-reorderDropped) and [`reorderCancelled`](https://slotsmith.dev/components/data-table/api/#label-reorderCancelled) are the announcements; the first three are functions of the position and the total.

## Motion and styling

Rows slide with transforms while a row is dragged, and glide into place after a drop. When the reader prefers reduced motion nothing slides: the lifted rows are dimmed where they are, and a line marks where they will land.

Every body [`Row`](https://slotsmith.dev/components/data-table/api/#slot-Row) carries attributes a stylesheet can use:

- `data-row-id`: the row's id, always present.
- `data-dragging`: on the row being moved.
- `data-dragging-child`: on its visible sub-rows, which move with it.
- `data-drop-position`: `"before"` or `"after"`, on the row it would be dropped next to.
- `data-drop-edge`: `"before"` or `"after"`, on the row whose edge it would land against: the target for `"before"`, and for `"after"` the last visible row of the target's subtree. Draw a drop line here.

The fallback row already styles them. A design system's own row needs a rule such as `tr[data-dragging], tr[data-dragging-child]` with an opaque background, so the rows underneath do not show through, and the handle has `data-dragging` too.

## The drag handle

The handle is the [`DragHandle`](https://slotsmith.dev/components/data-table/api/#slot-DragHandle) slot. A replacement must spread every prop it receives onto a real `<button>`: the pointer, key and blur handlers, the `aria-*` attributes, `disabled` and `style`.

- [`reorderHandleColumn`](https://slotsmith.dev/components/data-table/api/#props) set to `false` drops the leading handle column; place the handle in a cell of your own with `DataTable.DragHandle`, for example `{ id: "move", header: "", cell: () => <DataTable.DragHandle /> }`.
- [`slotProps`](https://slotsmith.dev/components/data-table/api/#props)`.dragHandle` adds props to every handle, per row. Its event handlers run after the table's own, not instead of them, so a handler that tracks a press keeps the drag working.

## Sorting and custom rows

A drop while a column is sorted means what it shows: the order on screen, with the row moved, becomes the new `data`, and the sort is cleared through `onSortingChange`, so the rows stay where they were dropped. With [`reorderWhileSorted`](https://slotsmith.dev/components/data-table/api/#props) set to `"block"`, the handles are disabled instead until the sort is cleared. A table that is only ordered by hand can turn sorting off, as both demos do with `enableSorting: false`. With pagination, a row moves within its page.

A drag survives its rows being re-created: when a custom [`Row`](https://slotsmith.dev/components/data-table/api/#slot-Row) slot is defined during render, React makes every row anew on each render, and the drag finds its row and handle again (a keyboard drag keeps focus on the handle). It is still slow, and it resets any state inside the rows, so define the slot once, outside the component that renders the table; a development warning says so.

To reorder rows in markup of your own, the [headless hook](https://slotsmith.dev/components/data-table/guides/headless-hook/) returns the same engine as `reorder`.
