Skip to content

Row DnD Table

Drag-and-drop row reordering with composable primitives.

Open in
Preview with Controlled State
Open in

For large datasets, use DataTableVirtualizedDndBody which combines row virtualization with drag-and-drop. Only visible rows are rendered in the DOM for optimal performance.

Open in

Row DnD lets users reorder table rows by dragging. Built with @dnd-kit and composable primitives that follow the shadcn/ui open-code pattern — copy the code, modify freely.

Install the DataTable core and row DnD add-on:

pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-row-dnd

First time using @niko-table? See the Installation Guide to set up the registry.

For other add-ons or manual copy-paste, see the Installation Guide.

We’ll build a task board with draggable rows:

type Task = {
id: string
title: string
status: "todo" | "in-progress" | "done" | "cancelled"
priority: "low" | "medium" | "high"
}

Three components work together:

  1. DataTableRowDndProvider — Wraps the table with DnD context and sensors
  2. DataTableDndBody — Renders rows as draggable items
  3. DataTableRowDragHandle — A grip icon button for dragging
row-dnd.tsx
const [data, setData] = React.useState(initialData)
const columns: DataTableColumnDef<Task>[] = [
{
id: "drag-handle",
size: 40,
header: () => null,
cell: ({ row }) => <DataTableRowDragHandle rowId={row.id} />,
enableSorting: false,
enableHiding: false,
},
// ... other columns
]
return (
<DataTableRoot data={data} columns={columns} getRowId={(row) => row.id}>
<DataTableRowDndProvider data={data} onReorder={setData}>
<DataTable>
<DataTableHeader />
<DataTableDndBody />
</DataTable>
</DataTableRowDndProvider>
</DataTableRoot>
)
  • getRowId is required — use stable, unique IDs (e.g., database IDs), not array indexes. Array indexes break DnD after reordering because the index no longer matches the original item
  • DataTableRowDndProvider must wrap outside <DataTable> — DnD context creates <div> elements that can’t be inside <table>
  • onReorder receives the new data array after arrayMove — just pass setData

The drag handle is a dedicated column with DataTableRowDragHandle:

{
id: "drag-handle",
size: 40,
header: () => null,
cell: ({ row }) => <DataTableRowDragHandle rowId={row.id} />,
enableSorting: false,
enableHiding: false,
}

Track the data order externally:

row-dnd-state.tsx
const [data, setData] = React.useState(initialData)
// Reset to original order
const resetData = () => setData(initialData)
return (
<DataTableRoot data={data} columns={columns} getRowId={(row) => row.id}>
<DataTableRowDndProvider data={data} onReorder={setData}>
<DataTable>
<DataTableHeader />
<DataTableDndBody />
</DataTable>
</DataTableRowDndProvider>
</DataTableRoot>
)

Don’t combine sorting or filtering with row DnD. Sorting and filtering override the manual row order — if a user drags row 3 to position 1, then a sort or filter resets it, the reorder is lost.

  • Avoid DataTableColumnSortMenu, DataTableSearchFilter, and DataTableFacetedFilter in DnD tables
  • If you need search, consider filtering the source data before passing it to the table
  • Column DnD is safe to combine with sorting/filtering since column order is independent of data order

✅ Use Row DnD when:

  • Users need to manually prioritize or reorder items
  • Building kanban boards, task lists, or playlist managers
  • Order matters and should be persisted

❌ Consider other options when:

  • Data has a natural sort order (use sorting instead)
  • The table is read-only and order doesn’t matter