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.

Reorder rowStepOwnerEstimate
Write the release notesLena Park2h
Freeze the main branchYuki Tanaka15m
Run the full test suiteDiego Alvarez1h
Tag the versionYuki Tanaka5m
Publish the packageSofia Reyes10m
Announce the releaseOmar Haddad30m

Drag a handle, or focus one and press Space.

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).
  • 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, 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; 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. For a sub-row, data is the unchanged top level: store siblings as the children of parent, as placeSiblings does here.

Reorder rowTaskOwner
Design
Omar Haddad
Sketch the flows
Priya Nair
Review with support
Jonas Weber
Final mockups
Omar Haddad
Build
Lena Park
Launch
Hana Kim

Open sub-rows move with their row.

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 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 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.

Reorder rowCardOwner
Write the release notesLena Park
Freeze the main branchYuki Tanaka
Run the full test suiteDiego Alvarez
Reorder rowCardOwner
Tag the versionYuki Tanaka

Drag a card into the other table, or lift one with Space and press ↓ past the last row.

Each table needs enableRowReorder, and reorderTableId names it in transfers. A drop calls onRowTransfer 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, 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.

Reorder rowTaskOwner
Design
Omar Haddad
Sketch the flows
Priya Nair
Review with support
Jonas Weber
Build
Lena Park
Database changes
Diego Alvarez
API endpoints
Lena Park
Launch
Hana Kim

Drag a task sideways to move it in or out a level.

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, 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 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 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:

KeysAction
SpaceEnterLift 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.
SpaceEnterDrop the row in its new place. Focus stays on its handle.
EscCancel. The row goes back where it was.
TabCancel, 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 translates them and labels replaces any of them: reorderRow names each handle, reorderInstructions is the instructions, and reorderLifted, reorderMoved, reorderDropped and 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 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 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 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.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 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 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 returns the same engine as reorder.