Search docs

Jump to any table example

Discord
ShadTable

A collection of composable table components built on shadcn/ui and TanStack Table.

Reorderable Table

Drag rows to reorder them, built on @dnd-kit/sortable rather than any table-specific drag logic. Column headers stay put — for drag-to-reorder columns, see Resizable / Reorderable Columns.

npx shadcn add https://shad-table.dev/r/reorderable-table.json
TitlePriorityStatus
Design landing pageHighIn progress
Set up CI pipelineMediumTodo
Write onboarding docsLowTodo
Fix pagination bugHighIn progress
Add dark mode toggleMediumDone
Audit accessibilityLowTodo

How it works

1.Row order is just a sortable list of ids

@dnd-kit/sortable doesn't know anything about tables — it only knows about an ordered array of ids and a strategy for laying them out (vertical, in this case). Row order lives directly in the data array itself; dataIds is just that array's ids, recomputed whenever data changes.

src/components/reorder/data-table.tsx
const dataIds = useMemo(() => data.map((row) => row.id), [data])

2.A drag handle, not the whole row, starts the drag

useSortable gives back attributes/listeners that must land on the actual draggable element. Spreading them only onto the GripVertical button — not the row itself — keeps clicking a cell's text or a header's sort control working normally; only the handle initiates a drag.

src/components/reorder/data-table.tsx
<button
type="button"
{...attributes}
{...listeners}
aria-label="Reorder row"
>
<GripVertical className="h-3.5 w-3.5" />
</button>

3.Dragging updates real state, not just visual position

onDragEnd computes the old and new index from the dragged id and calls arrayMove — the same helper dnd-kit ships for this exact case — then calls the parent's onDataChange so the reorder is visible to whoever owns the data, not just the table.

src/components/reorder/data-table.tsx
function handleRowDragEnd(event: DragEndEvent) {
const { active, over } = event
if (!over || active.id === over.id) return
const oldIndex = dataIds.indexOf(active.id as string)
const newIndex = dataIds.indexOf(over.id as string)
onDataChange(arrayMove(data, oldIndex, newIndex))
}

4.DndContext wraps the whole Table, not its header or body

restrictToVerticalAxis keeps drags from drifting sideways. DndContext has to wrap the entire Table from outside — it renders its own hidden accessibility nodes, and a <table> can only contain <thead>/<tbody>, so nesting a DndContext directly inside one breaks the HTML.

src/components/reorder/data-table.tsx
<DndContext modifiers={[restrictToVerticalAxis]} onDragEnd={handleRowDragEnd}>
<Table>
<TableHeader>{/* plain, non-draggable headers */}</TableHeader>
<TableBody>{/* draggable rows */}</TableBody>
</Table>
</DndContext>

5.getRowId keeps identity stable across reorders

TanStack Table defaults to using array index as a row's id, which breaks the moment you reorder the underlying array — row 2 becomes row 3 and loses its identity. getRowId: (row) => row.id pins each row's identity to its own data, so sorting, selection, and the drag state all keep tracking the right row after a reorder.

src/components/reorder/data-table.tsx
const table = useReactTable({
data,
columns,
getRowId: (row) => row.id,
// ...
})