Column DnD Table
Drag-and-drop column reordering with composable primitives.
Overview
Section titled “Overview”"use client"
import * as React from "react"import { DataTableRoot } from "@/components/niko-table/core/data-table-root"import { DataTable } from "@/components/niko-table/core/data-table"import { DataTableEmptyBody } from "@/components/niko-table/core/data-table-structure"import { DataTableDndHeader, DataTableDndColumnBody,} from "@/components/niko-table/core/data-table-dnd-structure"import { DataTableColumnDndProvider } from "@/components/niko-table/components/data-table-column-dnd"import { DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyTitle, DataTableEmptyDescription,} from "@/components/niko-table/components/data-table-empty-state"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Badge } from "@/components/ui/badge"import { Inbox } from "lucide-react"
// Typestype Employee = { id: string name: string department: string role: string location: string status: "active" | "on-leave" | "remote"}
// Sample dataconst data: Employee[] = [ { id: "EMP-001", name: "Alice Johnson", department: "Engineering", role: "Senior Developer", location: "New York", status: "active", }, { id: "EMP-002", name: "Bob Smith", department: "Design", role: "UI Designer", location: "San Francisco", status: "remote", }, { id: "EMP-003", name: "Carol Williams", department: "Marketing", role: "Marketing Lead", location: "Chicago", status: "active", }, { id: "EMP-004", name: "David Brown", department: "Engineering", role: "Backend Developer", location: "Austin", status: "on-leave", }, { id: "EMP-005", name: "Eva Martinez", department: "Product", role: "Product Manager", location: "Seattle", status: "active", }, { id: "EMP-006", name: "Frank Lee", department: "Engineering", role: "DevOps Engineer", location: "Denver", status: "remote", }, { id: "EMP-007", name: "Grace Kim", department: "Design", role: "UX Researcher", location: "Portland", status: "active", }, { id: "EMP-008", name: "Henry Davis", department: "Sales", role: "Account Executive", location: "Boston", status: "active", },]
// Status badge variant helperconst getStatusVariant = (status: Employee["status"]) => { switch (status) { case "active": return "default" case "remote": return "secondary" case "on-leave": return "outline" default: return "secondary" }}
// Column definitionsconst columns: DataTableColumnDef<Employee>[] = [ { accessorKey: "id", id: "id", header: "Employee ID", size: 120, }, { accessorKey: "name", id: "name", header: "Name", size: 160, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "department", id: "department", header: "Department", size: 140, }, { accessorKey: "role", id: "role", header: "Role", size: 170, }, { accessorKey: "location", id: "location", header: "Location", size: 140, }, { accessorKey: "status", id: "status", header: "Status", size: 110, cell: ({ row }) => { const status = row.getValue("status") as Employee["status"] return <Badge variant={getStatusVariant(status)}>{status}</Badge> }, },]
export default function ColumnDndExample() { const [columnOrder, setColumnOrder] = React.useState(() => columns.map(c => c.id as string), )
return ( <DataTableRoot data={data} columns={columns} state={{ columnOrder }} onColumnOrderChange={setColumnOrder} > <DataTableColumnDndProvider columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} > <DataTable> <DataTableDndHeader /> <DataTableDndColumnBody> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <Inbox className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No employees found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no employees to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> </DataTableEmptyBody> </DataTableDndColumnBody> </DataTable> </DataTableColumnDndProvider> </DataTableRoot> )}Preview with Controlled State
Current Column Order
Drag column headers to reorder. The order is tracked in state.
Total Columns:6
Current Order:id → name → department → role → location → status
View Full State
[ "id", "name", "department", "role", "location", "status" ]
"use client"
import * as React from "react"import { DataTableRoot } from "@/components/niko-table/core/data-table-root"import { DataTable } from "@/components/niko-table/core/data-table"import { DataTableEmptyBody } from "@/components/niko-table/core/data-table-structure"import { DataTableDndHeader, DataTableDndColumnBody,} from "@/components/niko-table/core/data-table-dnd-structure"import { DataTableColumnDndProvider } from "@/components/niko-table/components/data-table-column-dnd"import { DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyTitle, DataTableEmptyDescription,} from "@/components/niko-table/components/data-table-empty-state"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Badge } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"import { Inbox } from "lucide-react"
// Typestype Employee = { id: string name: string department: string role: string location: string status: "active" | "on-leave" | "remote"}
// Sample dataconst data: Employee[] = [ { id: "EMP-001", name: "Alice Johnson", department: "Engineering", role: "Senior Developer", location: "New York", status: "active", }, { id: "EMP-002", name: "Bob Smith", department: "Design", role: "UI Designer", location: "San Francisco", status: "remote", }, { id: "EMP-003", name: "Carol Williams", department: "Marketing", role: "Marketing Lead", location: "Chicago", status: "active", }, { id: "EMP-004", name: "David Brown", department: "Engineering", role: "Backend Developer", location: "Austin", status: "on-leave", }, { id: "EMP-005", name: "Eva Martinez", department: "Product", role: "Product Manager", location: "Seattle", status: "active", }, { id: "EMP-006", name: "Frank Lee", department: "Engineering", role: "DevOps Engineer", location: "Denver", status: "remote", }, { id: "EMP-007", name: "Grace Kim", department: "Design", role: "UX Researcher", location: "Portland", status: "active", }, { id: "EMP-008", name: "Henry Davis", department: "Sales", role: "Account Executive", location: "Boston", status: "active", },]
// Status badge variant helperconst getStatusVariant = (status: Employee["status"]) => { switch (status) { case "active": return "default" case "remote": return "secondary" case "on-leave": return "outline" default: return "secondary" }}
// Column definitionsconst columns: DataTableColumnDef<Employee>[] = [ { accessorKey: "id", id: "id", header: "Employee ID", size: 120, }, { accessorKey: "name", id: "name", header: "Name", size: 160, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "department", id: "department", header: "Department", size: 140, }, { accessorKey: "role", id: "role", header: "Role", size: 170, }, { accessorKey: "location", id: "location", header: "Location", size: 140, }, { accessorKey: "status", id: "status", header: "Status", size: 110, cell: ({ row }) => { const status = row.getValue("status") as Employee["status"] return <Badge variant={getStatusVariant(status)}>{status}</Badge> }, },]
const initialColumnOrder = columns.map(c => c.id as string)
export default function ColumnDndStateExample() { const [columnOrder, setColumnOrder] = React.useState<string[]>(initialColumnOrder)
const resetColumnOrder = () => setColumnOrder(initialColumnOrder)
return ( <div className="w-full space-y-4"> <DataTableRoot data={data} columns={columns} state={{ columnOrder }} onColumnOrderChange={setColumnOrder} > <DataTableColumnDndProvider columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} > <DataTable> <DataTableDndHeader /> <DataTableDndColumnBody> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <Inbox className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No employees found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no employees to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> </DataTableEmptyBody> </DataTableDndColumnBody> </DataTable> </DataTableColumnDndProvider> </DataTableRoot>
{/* State Display */} <Card> <CardHeader> <CardTitle>Current Column Order</CardTitle> <CardDescription> Drag column headers to reorder. The order is tracked in state. </CardDescription> <CardAction> <Button variant="outline" size="sm" onClick={resetColumnOrder}> Reset Order </Button> </CardAction> </CardHeader> <CardContent> <div className="grid gap-2 text-xs text-muted-foreground"> <div className="flex justify-between"> <span className="font-medium">Total Columns:</span> <span className="text-foreground">{columnOrder.length}</span> </div> <div className="flex justify-between"> <span className="font-medium">Current Order:</span> <span className="text-foreground">{columnOrder.join(" → ")}</span> </div> </div>
<details className="mt-4 border-t pt-4"> <summary className="cursor-pointer text-xs font-medium hover:text-foreground"> View Full State </summary> <pre className="mt-2 overflow-auto rounded bg-muted p-2 text-xs"> {JSON.stringify(columnOrder, null, 2)} </pre> </details> </CardContent> </Card> </div> )}Virtualized Column DnD
Section titled “Virtualized Column DnD”For large datasets, use DataTableVirtualizedDndHeader and DataTableVirtualizedDndColumnBody which combine row virtualization with column drag-and-drop.
"use client"
import * as React from "react"import { DataTableRoot } from "@/components/niko-table/core/data-table-root"import { DataTable } from "@/components/niko-table/core/data-table"import { DataTableVirtualizedEmptyBody } from "@/components/niko-table/core/data-table-virtualized-structure"import { DataTableVirtualizedDndHeader, DataTableVirtualizedDndColumnBody,} from "@/components/niko-table/core/data-table-virtualized-dnd-structure"import { DataTableColumnDndProvider } from "@/components/niko-table/components/data-table-column-dnd"import { DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyTitle, DataTableEmptyDescription,} from "@/components/niko-table/components/data-table-empty-state"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Badge } from "@/components/ui/badge"import { Inbox } from "lucide-react"
// Typestype Employee = { id: string name: string email: string department: string role: string location: string status: "active" | "on-leave" | "remote" salary: number}
// Generate large dataset for virtualization democonst generateEmployees = (count: number): Employee[] => { const firstNames = [ "Alice", "Bob", "Carol", "David", "Eva", "Frank", "Grace", "Henry", "Ivy", "Jack", "Karen", "Leo", "Mia", "Noah", "Olivia", "Paul", "Quinn", "Ruby", "Sam", "Tina", ] const lastNames = [ "Johnson", "Smith", "Williams", "Brown", "Martinez", "Lee", "Kim", "Davis", "Wilson", "Taylor", "Anderson", "Thomas", "Jackson", "White", "Harris", ] const departments = [ "Engineering", "Design", "Marketing", "Sales", "Product", "HR", "Finance", "Operations", ] const roles = [ "Senior Developer", "UI Designer", "Marketing Lead", "Backend Developer", "Product Manager", "DevOps Engineer", "UX Researcher", "Account Executive", "Data Analyst", "QA Engineer", ] const locations = [ "New York", "San Francisco", "Chicago", "Austin", "Seattle", "Denver", "Portland", "Boston", "Miami", "Atlanta", ] const statuses: Employee["status"][] = ["active", "on-leave", "remote"]
return Array.from({ length: count }, (_, i) => { const firstName = firstNames[Math.floor(Math.random() * firstNames.length)] const lastName = lastNames[Math.floor(Math.random() * lastNames.length)] return { id: `EMP-${String(i + 1).padStart(4, "0")}`, name: `${firstName} ${lastName}`, email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${i}@company.com`, department: departments[Math.floor(Math.random() * departments.length)], role: roles[Math.floor(Math.random() * roles.length)], location: locations[Math.floor(Math.random() * locations.length)], status: statuses[Math.floor(Math.random() * statuses.length)], salary: Math.floor(Math.random() * 100000) + 50000, } })}
const data = generateEmployees(500)
// Status badge variant helperconst getStatusVariant = (status: Employee["status"]) => { switch (status) { case "active": return "default" case "remote": return "secondary" case "on-leave": return "outline" default: return "secondary" }}
// Column definitions — all columns need explicit `id` for column orderingconst columns: DataTableColumnDef<Employee>[] = [ { accessorKey: "id", id: "id", header: "Employee ID", size: 120, }, { accessorKey: "name", id: "name", header: "Name", size: 170, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "email", id: "email", header: "Email", size: 250, cell: ({ row }) => ( <div className="text-muted-foreground">{row.getValue("email")}</div> ), }, { accessorKey: "department", id: "department", header: "Department", size: 140, }, { accessorKey: "role", id: "role", header: "Role", size: 170, }, { accessorKey: "location", id: "location", header: "Location", size: 140, }, { accessorKey: "status", id: "status", header: "Status", size: 110, cell: ({ row }) => { const status = row.getValue("status") as Employee["status"] return <Badge variant={getStatusVariant(status)}>{status}</Badge> }, }, { accessorKey: "salary", id: "salary", header: "Salary", size: 120, cell: ({ row }) => { const salary = row.getValue("salary") as number return <div className="font-mono">${salary.toLocaleString()}</div> }, },]
export default function VirtualizedColumnDndExample() { const [columnOrder, setColumnOrder] = React.useState(() => columns.map(c => c.id as string), )
return ( <DataTableRoot data={data} columns={columns} state={{ columnOrder }} onColumnOrderChange={setColumnOrder} > <DataTableColumnDndProvider columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} > <DataTable height={500}> <DataTableVirtualizedDndHeader /> <DataTableVirtualizedDndColumnBody estimateSize={40} overscan={10}> <DataTableVirtualizedEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <Inbox className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No employees found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no employees to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> </DataTableVirtualizedEmptyBody> </DataTableVirtualizedDndColumnBody> </DataTable> </DataTableColumnDndProvider> </DataTableRoot> )}Introduction
Section titled “Introduction”Column DnD lets users reorder table columns by dragging headers. Built with @dnd-kit and composable primitives following the shadcn/ui open-code pattern.
Installation
Section titled “Installation”Install the DataTable core and column DnD add-on:
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.
Basic Column DnD
Section titled “Basic Column DnD”Three components work together:
DataTableColumnDndProvider— Wraps the table with horizontal DnD contextDataTableDndHeader— Renders headers as draggable itemsDataTableDndColumnBody— Cells follow column drag position
const [columnOrder, setColumnOrder] = React.useState<string[]>(() => columns.map(c => c.id as string),)
return ( <DataTableRoot data={data} columns={columns} state={{ columnOrder }} onColumnOrderChange={setColumnOrder} > <DataTableColumnDndProvider columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} > <DataTable> <DataTableDndHeader /> <DataTableDndColumnBody /> </DataTable> </DataTableColumnDndProvider> </DataTableRoot>)Key Points
Section titled “Key Points”- Every column needs an explicit
idfield for column order tracking columnOrderstate must be passed to bothDataTableRootandDataTableColumnDndProvider- Headers are draggable by default — grab any header to reorder
- Safe to combine with sorting and filtering — column order is independent of data order, unlike Row DnD
When to Use
Section titled “When to Use”✅ Use Column DnD when:
- Users want to customize their table layout
- Different users prefer different column arrangements
- Building configurable dashboards
❌ Consider other options when:
- Column order is fixed by design
- Tables have very few columns
Next Steps
Section titled “Next Steps”- Row DnD Table — Drag-and-drop row reordering
- Virtualization Table — Virtual scrolling for large datasets
- Column Pinning Table — Pin columns to edges