Drag-and-drop row & column reordering for React tables.
60fps animations · Auto-scroll · Mobile long-press · Virtual scrolling · Zero UI deps
- Rows & columns — reorder both independently, automatic direction detection
- 60fps — direct DOM transforms during drag, no React re-renders until drop
- Mobile — long-press to drag on touch devices, optimized for Chrome Android & Safari iOS
- Auto-scroll — accelerates when dragging near container edges
- 100k+ rows — works with
@tanstack/react-virtual - Drag handles — restrict drag to a grip icon with
<DragHandle> - Constraints — lock specific rows or columns via drag range options
- Drop animation — clone smoothly flies to the drop target
- Fully styleable —
className+styleon every component — Tailwind, styled-components, CSS modules - TypeScript — full type definitions out of the box
- Tiny — only peer dependency is React
npm install react-table-dndRequires
reactandreact-dom>= 18.0.0
Then import the styles once in your app entry (e.g. main.tsx):
import 'react-table-dnd/styles'import {
TableContainer, TableHeader, ColumnCell,
TableBody, BodyRow, RowCell,
} from "react-table-dnd";
function arrayMove(arr, from, to) {
const next = [...arr];
const [item] = next.splice(from, 1);
next.splice(to, 0, item);
return next;
}
export default function App() {
const [cols, setCols] = useState([
{ id: "name", label: "Name", width: 150 },
{ id: "age", label: "Age", width: 100 },
{ id: "city", label: "City", width: 160 },
]);
const [rows, setRows] = useState([
{ id: "1", name: "Alice", age: 28, city: "NYC" },
{ id: "2", name: "Bob", age: 34, city: "LA" },
{ id: "3", name: "Carol", age: 22, city: "SF" },
]);
return (
<TableContainer
onDragEnd={({ sourceIndex, targetIndex, dragType }) => {
if (dragType === "column") setCols(arrayMove(cols, sourceIndex, targetIndex));
else setRows(arrayMove(rows, sourceIndex, targetIndex));
}}
>
<TableHeader>
{cols.map((col, i) => (
<ColumnCell key={col.id} id={col.id} index={i} style={{ width: col.width }}>
{col.label}
</ColumnCell>
))}
</TableHeader>
<TableBody>
{rows.map((row, ri) => (
<BodyRow key={row.id} id={row.id} index={ri}>
{cols.map((col, ci) => (
<RowCell key={col.id} index={ci}>
{row[col.id]}
</RowCell>
))}
</BodyRow>
))}
</TableBody>
</TableContainer>
);
}| Component | Props | Description |
|---|---|---|
TableContainer |
onDragEnd, options, renderPlaceholder, className, style |
Root wrapper — provides drag context |
TableHeader |
className, style |
Header row container |
ColumnCell |
id, index, className, style |
Draggable column header cell |
TableBody |
className, style |
Scrollable body — pass ref for virtual scrolling |
BodyRow |
id, index, className, style |
Draggable row |
RowCell |
index, className, style |
Cell within a row |
DragHandle |
className, style |
Wrap inside BodyRow/ColumnCell to restrict drag to this element |
Bold props are required.
Pass width inside the style prop on ColumnCell. Columns grow proportionally by default to fill available space. To fix a column at exactly its pixel size, also pass flex:
{/* Flex — grows proportionally to fill container (default) */}
<ColumnCell style={{ width: 150 }}>Name</ColumnCell>
{/* Fixed — stays exactly 150px regardless of container width */}
<ColumnCell style={{ width: 150, flex: "0 0 150px" }}>Name</ColumnCell>interface DragEndResult {
sourceIndex: number;
targetIndex: number;
dragType: "row" | "column";
}
interface DragRange {
start?: number; // first draggable index
end?: number; // last draggable index (exclusive)
}<TableContainer
options={{
rowDragRange: { start: 1 }, // lock first row
columnDragRange: { start: 1, end: 5 }, // lock first col, only 1-4 draggable
}}
/>import { DragHandle } from "react-table-dnd";
<BodyRow id="1" index={0}>
<RowCell index={0}>
<DragHandle><GripIcon /></DragHandle>
Content here
</RowCell>
</BodyRow><TableContainer
renderPlaceholder={() => (
<div style={{
background: "#6366f122",
border: "2px dashed #6366f1",
height: "100%",
}} />
)}
/>Every component accepts className and style. No opinionated styles on cells.
| Inline | Tailwind | styled-components |
<ColumnCell style={{
padding: "0 16px",
fontWeight: 700,
}} /> |
<ColumnCell className="px-4
font-bold text-sm" /> |
const Col = styled(ColumnCell)`
padding: 0 16px;
font-weight: 700;
`; |
| Chrome | Firefox | Safari | Edge | |
|---|---|---|---|---|
| Desktop | ✅ | ✅ | ✅ | ✅ |
| Mobile | ✅ | ✅ | ✅ | ✅ |
Mobile uses long-press to initiate drag.
git clone https://github.com/samiodeh1337/react-table-dnd.git
cd react-table-dnd
npm install
npm run dev # docs site at localhost:5173