Skip to content

Row DnD Table

Drag-and-drop row reordering with composable primitives.

Reorder rows via drag-and-drop using DataTableRowDndProvider and a dedicated drag-handle column. Important constraint: do not combine row DnD with sorting or filtering — the user’s manual order will fight whatever the table is computing, so disable those add-ons on a DnD table. Column resize is fine — drop in <DataTableColumnResize /> (same as other tables); keep enableResizing: false on the drag-handle / # columns.

Open in
#
Task ID
Title
Status
Priority
1TASK-001Set up project repositorydonehigh
2TASK-002Design database schemain-progresshigh
3TASK-003Implement authenticationtodomedium
4TASK-004Create API endpointstodomedium
5TASK-005Write unit teststodolow
6TASK-006Set up CI/CD pipelinecancelledlow
7TASK-007Deploy to stagingtodomedium
8TASK-008Performance optimizationin-progresshigh
Preview with Controlled State
Open in
#
Task ID
Title
Status
Priority
1TASK-001Set up project repositorydonehigh
2TASK-002Design database schemain-progresshigh
3TASK-003Implement authenticationtodomedium
4TASK-004Create API endpointstodomedium
5TASK-005Write unit teststodolow
6TASK-006Set up CI/CD pipelinecancelledlow
7TASK-007Deploy to stagingtodomedium
8TASK-008Performance optimizationin-progresshigh
Current Row Order
Drag rows to reorder. The order is tracked in state.
Total Items:8
Current Order:TASK-001 → TASK-002 → TASK-003 → TASK-004 → TASK-005 → TASK-006 → TASK-007 → TASK-008
View Full State
[
  {
    "id": "TASK-001",
    "title": "Set up project repository"
  },
  {
    "id": "TASK-002",
    "title": "Design database schema"
  },
  {
    "id": "TASK-003",
    "title": "Implement authentication"
  },
  {
    "id": "TASK-004",
    "title": "Create API endpoints"
  },
  {
    "id": "TASK-005",
    "title": "Write unit tests"
  },
  {
    "id": "TASK-006",
    "title": "Set up CI/CD pipeline"
  },
  {
    "id": "TASK-007",
    "title": "Deploy to staging"
  },
  {
    "id": "TASK-008",
    "title": "Performance optimization"
  }
]

For large datasets, use DataTableVirtualizedFlexHeader + DataTableVirtualizedDndBody which combine row virtualization with drag-and-drop. Only visible rows are rendered in the DOM for optimal performance. The same <DataTableColumnResize /> marker works here — grips appear on the flex header.

Open in
#
Task ID
Title
Assignee
Status
Priority

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 @niko-table/data-table-column-resize @niko-table/data-table-virtualized @niko-table/data-table-virtualized-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
import { DataTableDndBody } from "@/components/niko-table/core/data-table-row-dnd-structure"
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,
enableResizing: false,
},
// ... other columns
]
return (
<DataTableRoot data={data} columns={columns} getRowId={(row) => row.id}>
<DataTableColumnResize />
<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
import { DataTableDndBody } from "@/components/niko-table/core/data-table-row-dnd-structure"
const [data, setData] = React.useState(initialData)
// Reset to original order
const resetData = () => setData(initialData)
return (
<DataTableRoot data={data} columns={columns} getRowId={(row) => row.id}>
<DataTableColumnResize />
<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