Column DnD Table
Drag-and-drop column reordering with composable primitives.
Drag column headers to reorder them, wired through DataTableColumnDndProvider on top of @dnd-kit. Unlike row DnD, column DnD is safe to combine with sorting, filtering, and virtualization — the user’s column order is independent of the row model.
Overview
Section titled “Overview”| Employee ID | Name | Department | Role | Location | Status |
|---|---|---|---|---|---|
| EMP-001 | Alice Johnson | Engineering | Senior Developer | New York | active |
| EMP-002 | Bob Smith | Design | UI Designer | San Francisco | remote |
| EMP-003 | Carol Williams | Marketing | Marketing Lead | Chicago | active |
| EMP-004 | David Brown | Engineering | Backend Developer | Austin | on-leave |
| EMP-005 | Eva Martinez | Product | Product Manager | Seattle | active |
| EMP-006 | Frank Lee | Engineering | DevOps Engineer | Denver | remote |
| EMP-007 | Grace Kim | Design | UX Researcher | Portland | active |
| EMP-008 | Henry Davis | Sales | Account Executive | Boston | active |
1"use client"2
3import * as React from "react"4import { DataTableRoot } from "@/components/niko-table/core/data-table-root"5import { DataTable } from "@/components/niko-table/core/data-table"6import { DataTableEmptyBody } from "@/components/niko-table/core/data-table-structure"7import {8 DataTableDndHeader,9 DataTableDndColumnBody,10} from "@/components/niko-table/core/data-table-column-dnd-structure"11import { DataTableColumnDndProvider } from "@/components/niko-table/components/data-table-column-dnd"12import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"13import {14 DataTableEmptyIcon,15 DataTableEmptyMessage,16 DataTableEmptyTitle,17 DataTableEmptyDescription,18} from "@/components/niko-table/components/data-table-empty-state"19import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"20import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"21import { DataTableViewDndMenu } from "@/components/niko-table/components/data-table-view-dnd-menu"22import type { DataTableColumnDef } from "@/components/niko-table/types"23import { Badge } from "@/components/ui/badge"24import { Inbox } from "lucide-react"25
26// Types27type Employee = {28 id: string29 name: string30 department: string31 role: string32 location: string33 status: "active" | "on-leave" | "remote"34}35
36// Sample data37const data: Employee[] = [38 {39 id: "EMP-001",40 name: "Alice Johnson",41 department: "Engineering",42 role: "Senior Developer",43 location: "New York",44 status: "active",45 },46 {47 id: "EMP-002",48 name: "Bob Smith",49 department: "Design",50 role: "UI Designer",51 location: "San Francisco",52 status: "remote",53 },54 {55 id: "EMP-003",56 name: "Carol Williams",57 department: "Marketing",58 role: "Marketing Lead",59 location: "Chicago",60 status: "active",61 },62 {63 id: "EMP-004",64 name: "David Brown",65 department: "Engineering",66 role: "Backend Developer",67 location: "Austin",68 status: "on-leave",69 },70 {71 id: "EMP-005",72 name: "Eva Martinez",73 department: "Product",74 role: "Product Manager",75 location: "Seattle",76 status: "active",77 },78 {79 id: "EMP-006",80 name: "Frank Lee",81 department: "Engineering",82 role: "DevOps Engineer",83 location: "Denver",84 status: "remote",85 },86 {87 id: "EMP-007",88 name: "Grace Kim",89 department: "Design",90 role: "UX Researcher",91 location: "Portland",92 status: "active",93 },94 {95 id: "EMP-008",96 name: "Henry Davis",97 department: "Sales",98 role: "Account Executive",99 location: "Boston",100 status: "active",101 },102]103
104// Status badge variant helper105const getStatusVariant = (status: Employee["status"]) => {106 switch (status) {107 case "active":108 return "default"109 case "remote":110 return "secondary"111 case "on-leave":112 return "outline"113 default:114 return "secondary"115 }116}117
118// Column definitions119const columns: DataTableColumnDef<Employee>[] = [120 {121 accessorKey: "id",122 id: "id",123 header: "Employee ID",124 size: 120,125 },126 {127 accessorKey: "name",128 id: "name",129 header: "Name",130 size: 160,131 cell: ({ row }) => (132 <div className="font-medium">{row.getValue("name")}</div>133 ),134 },135 {136 accessorKey: "department",137 id: "department",138 header: "Department",139 size: 140,140 },141 {142 accessorKey: "role",143 id: "role",144 header: "Role",145 size: 170,146 },147 {148 accessorKey: "location",149 id: "location",150 header: "Location",151 size: 140,152 },153 {154 accessorKey: "status",155 id: "status",156 header: "Status",157 size: 110,158 cell: ({ row }) => {159 const status = row.getValue("status") as Employee["status"]160 return <Badge variant={getStatusVariant(status)}>{status}</Badge>161 },162 },163]164
165const initialColumnOrder = columns.map(c => c.id as string)166
167export default function ColumnDndExample() {168 const [columnOrder, setColumnOrder] =169 React.useState<string[]>(initialColumnOrder)170
171 return (172 <DataTableRoot173 data={data}174 columns={columns}175 state={{ columnOrder }}176 onColumnOrderChange={setColumnOrder}177 >178 <DataTableColumnResize />179 <DataTableColumnDndProvider180 columnOrder={columnOrder}181 onColumnOrderChange={setColumnOrder}182 >183 {/*184 * Two reorder surfaces share one `columnOrder`:185 * 1. Drag column headers (DataTableColumnDndProvider)186 * 2. Drag rows in the View menu (DataTableViewDndMenu)187 * Either updates the same state, so both stay in sync.188 */}189 <DataTableToolbarSection className="justify-between">190 <DataTableSearchFilter placeholder="Search employees..." />191 <DataTableViewDndMenu192 columnOrder={columnOrder}193 onColumnOrderChange={setColumnOrder}194 onReset={() => setColumnOrder(initialColumnOrder)}195 />196 </DataTableToolbarSection>197 <DataTable>198 <DataTableDndHeader />199 <DataTableDndColumnBody>200 <DataTableEmptyBody>201 <DataTableEmptyMessage>202 <DataTableEmptyIcon>203 <Inbox className="size-12" />204 </DataTableEmptyIcon>205 <DataTableEmptyTitle>No employees found</DataTableEmptyTitle>206 <DataTableEmptyDescription>207 Try a different search.208 </DataTableEmptyDescription>209 </DataTableEmptyMessage>210 </DataTableEmptyBody>211 </DataTableDndColumnBody>212 </DataTable>213 </DataTableColumnDndProvider>214 </DataTableRoot>215 )216}Preview with Controlled State
| Employee ID | Name | Department | Role | Location | Status |
|---|---|---|---|---|---|
| EMP-001 | Alice Johnson | Engineering | Senior Developer | New York | active |
| EMP-002 | Bob Smith | Design | UI Designer | San Francisco | remote |
| EMP-003 | Carol Williams | Marketing | Marketing Lead | Chicago | active |
| EMP-004 | David Brown | Engineering | Backend Developer | Austin | on-leave |
| EMP-005 | Eva Martinez | Product | Product Manager | Seattle | active |
| EMP-006 | Frank Lee | Engineering | DevOps Engineer | Denver | remote |
| EMP-007 | Grace Kim | Design | UX Researcher | Portland | active |
| EMP-008 | Henry Davis | Sales | Account Executive | Boston | active |
View Full State
{
"columnOrder": [
"id",
"name",
"department",
"role",
"location",
"status"
],
"columnVisibility": {},
"globalFilter": ""
}1"use client"2
3import * as React from "react"4import type { VisibilityState } from "@tanstack/react-table"5import { DataTableRoot } from "@/components/niko-table/core/data-table-root"6import { DataTable } from "@/components/niko-table/core/data-table"7import { DataTableEmptyBody } from "@/components/niko-table/core/data-table-structure"8import {9 DataTableDndHeader,10 DataTableDndColumnBody,11} from "@/components/niko-table/core/data-table-column-dnd-structure"12import { DataTableColumnDndProvider } from "@/components/niko-table/components/data-table-column-dnd"13import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"14import {15 DataTableEmptyIcon,16 DataTableEmptyMessage,17 DataTableEmptyTitle,18 DataTableEmptyDescription,19} from "@/components/niko-table/components/data-table-empty-state"20import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"21import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"22import { DataTableViewDndMenu } from "@/components/niko-table/components/data-table-view-dnd-menu"23import type { DataTableColumnDef } from "@/components/niko-table/types"24import { Badge } from "@/components/ui/badge"25import { Button } from "@/components/ui/button"26import {27 Card,28 CardAction,29 CardContent,30 CardDescription,31 CardHeader,32 CardTitle,33} from "@/components/ui/card"34import { Inbox } from "lucide-react"35
36// Types37type Employee = {38 id: string39 name: string40 department: string41 role: string42 location: string43 status: "active" | "on-leave" | "remote"44}45
46// Sample data47const data: Employee[] = [48 {49 id: "EMP-001",50 name: "Alice Johnson",51 department: "Engineering",52 role: "Senior Developer",53 location: "New York",54 status: "active",55 },56 {57 id: "EMP-002",58 name: "Bob Smith",59 department: "Design",60 role: "UI Designer",61 location: "San Francisco",62 status: "remote",63 },64 {65 id: "EMP-003",66 name: "Carol Williams",67 department: "Marketing",68 role: "Marketing Lead",69 location: "Chicago",70 status: "active",71 },72 {73 id: "EMP-004",74 name: "David Brown",75 department: "Engineering",76 role: "Backend Developer",77 location: "Austin",78 status: "on-leave",79 },80 {81 id: "EMP-005",82 name: "Eva Martinez",83 department: "Product",84 role: "Product Manager",85 location: "Seattle",86 status: "active",87 },88 {89 id: "EMP-006",90 name: "Frank Lee",91 department: "Engineering",92 role: "DevOps Engineer",93 location: "Denver",94 status: "remote",95 },96 {97 id: "EMP-007",98 name: "Grace Kim",99 department: "Design",100 role: "UX Researcher",101 location: "Portland",102 status: "active",103 },104 {105 id: "EMP-008",106 name: "Henry Davis",107 department: "Sales",108 role: "Account Executive",109 location: "Boston",110 status: "active",111 },112]113
114// Status badge variant helper115const getStatusVariant = (status: Employee["status"]) => {116 switch (status) {117 case "active":118 return "default"119 case "remote":120 return "secondary"121 case "on-leave":122 return "outline"123 default:124 return "secondary"125 }126}127
128// Column definitions129const columns: DataTableColumnDef<Employee>[] = [130 {131 accessorKey: "id",132 id: "id",133 header: "Employee ID",134 size: 120,135 },136 {137 accessorKey: "name",138 id: "name",139 header: "Name",140 size: 160,141 cell: ({ row }) => (142 <div className="font-medium">{row.getValue("name")}</div>143 ),144 },145 {146 accessorKey: "department",147 id: "department",148 header: "Department",149 size: 140,150 },151 {152 accessorKey: "role",153 id: "role",154 header: "Role",155 size: 170,156 },157 {158 accessorKey: "location",159 id: "location",160 header: "Location",161 size: 140,162 },163 {164 accessorKey: "status",165 id: "status",166 header: "Status",167 size: 110,168 cell: ({ row }) => {169 const status = row.getValue("status") as Employee["status"]170 return <Badge variant={getStatusVariant(status)}>{status}</Badge>171 },172 },173]174
175const initialColumnOrder = columns.map(c => c.id as string)176
177export default function ColumnDndStateExample() {178 const [columnOrder, setColumnOrder] =179 React.useState<string[]>(initialColumnOrder)180 const [columnVisibility, setColumnVisibility] =181 React.useState<VisibilityState>({})182 const [globalFilter, setGlobalFilter] = React.useState<string | object>("")183
184 const resetAll = () => {185 setColumnOrder(initialColumnOrder)186 setColumnVisibility({})187 setGlobalFilter("")188 }189
190 return (191 <div className="w-full space-y-4">192 <DataTableRoot193 data={data}194 columns={columns}195 state={{ columnOrder, columnVisibility, globalFilter }}196 onColumnOrderChange={setColumnOrder}197 onColumnVisibilityChange={setColumnVisibility}198 onGlobalFilterChange={setGlobalFilter}199 >200 <DataTableColumnResize />201 <DataTableColumnDndProvider202 columnOrder={columnOrder}203 onColumnOrderChange={setColumnOrder}204 >205 {/*206 * Two reorder surfaces share one `columnOrder`:207 * 1. Drag column headers (DataTableColumnDndProvider)208 * 2. Drag rows in the View menu (DataTableViewDndMenu)209 * Either updates the same state, so both stay in sync.210 * The View menu also toggles visibility and exposes a Reset.211 */}212 <DataTableToolbarSection className="justify-between">213 <DataTableSearchFilter placeholder="Search employees..." />214 <DataTableViewDndMenu215 columnOrder={columnOrder}216 onColumnOrderChange={setColumnOrder}217 onReset={resetAll}218 />219 </DataTableToolbarSection>220 <DataTable>221 <DataTableDndHeader />222 <DataTableDndColumnBody>223 <DataTableEmptyBody>224 <DataTableEmptyMessage>225 <DataTableEmptyIcon>226 <Inbox className="size-12" />227 </DataTableEmptyIcon>228 <DataTableEmptyTitle>No employees found</DataTableEmptyTitle>229 <DataTableEmptyDescription>230 Try a different search.231 </DataTableEmptyDescription>232 </DataTableEmptyMessage>233 </DataTableEmptyBody>234 </DataTableDndColumnBody>235 </DataTable>236 </DataTableColumnDndProvider>237 </DataTableRoot>238
239 {/* State Display */}240 <Card>241 <CardHeader>242 <CardTitle>Column DnD State</CardTitle>243 <CardDescription>244 Header drag, view-menu drag, visibility toggles, and search — all245 live state.246 </CardDescription>247 <CardAction>248 <Button variant="outline" size="sm" onClick={resetAll}>249 Reset250 </Button>251 </CardAction>252 </CardHeader>253 <CardContent>254 <div className="grid gap-2 text-xs text-muted-foreground">255 <div className="flex justify-between">256 <span className="font-medium">Total Columns:</span>257 <span className="text-foreground">{columnOrder.length}</span>258 </div>259 <div className="flex justify-between">260 <span className="font-medium">Current Order:</span>261 <span className="text-foreground">{columnOrder.join(" → ")}</span>262 </div>263 <div className="flex justify-between">264 <span className="font-medium">Hidden:</span>265 <span className="text-foreground">266 {Object.entries(columnVisibility)267 .filter(([, v]) => !v)268 .map(([k]) => k)269 .join(", ") || "none"}270 </span>271 </div>272 <div className="flex justify-between">273 <span className="font-medium">Search:</span>274 <span className="text-foreground">275 {typeof globalFilter === "string" && globalFilter276 ? globalFilter277 : "—"}278 </span>279 </div>280 </div>281
282 <details className="mt-4 border-t pt-4">283 <summary className="cursor-pointer text-xs font-medium hover:text-foreground">284 View Full State285 </summary>286 <pre className="mt-2 overflow-auto rounded bg-muted p-2 text-xs">287 {JSON.stringify(288 { columnOrder, columnVisibility, globalFilter },289 null,290 2,291 )}292 </pre>293 </details>294 </CardContent>295 </Card>296 </div>297 )298}Virtualized Column DnD
Section titled “Virtualized Column DnD”For large datasets, use DataTableVirtualizedDndHeader and DataTableVirtualizedDndColumnBody which combine row virtualization with column drag-and-drop. Same <DataTableColumnResize /> marker — drag a header to reorder, drag the edge grip to resize (grip stops propagation so it won’t start a reorder).
| Employee ID | Name | Email | Department | Role | Location | Status | Salary |
|---|
1"use client"2
3import * as React from "react"4import { DataTableRoot } from "@/components/niko-table/core/data-table-root"5import { DataTable } from "@/components/niko-table/core/data-table"6import { DataTableVirtualizedEmptyBody } from "@/components/niko-table/core/data-table-virtualized-structure"7import {8 DataTableVirtualizedDndHeader,9 DataTableVirtualizedDndColumnBody,10} from "@/components/niko-table/core/data-table-virtualized-column-dnd-structure"11import { DataTableColumnDndProvider } from "@/components/niko-table/components/data-table-column-dnd"12import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"13import {14 DataTableEmptyIcon,15 DataTableEmptyMessage,16 DataTableEmptyTitle,17 DataTableEmptyDescription,18} from "@/components/niko-table/components/data-table-empty-state"19import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"20import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"21import { DataTableViewDndMenu } from "@/components/niko-table/components/data-table-view-dnd-menu"22import type { DataTableColumnDef } from "@/components/niko-table/types"23import { Badge } from "@/components/ui/badge"24import { Inbox } from "lucide-react"25
26// Types27type Employee = {28 id: string29 name: string30 email: string31 department: string32 role: string33 location: string34 status: "active" | "on-leave" | "remote"35 salary: number36}37
38// Generate large dataset for virtualization demo39const generateEmployees = (count: number): Employee[] => {40 const firstNames = [41 "Alice",42 "Bob",43 "Carol",44 "David",45 "Eva",46 "Frank",47 "Grace",48 "Henry",49 "Ivy",50 "Jack",51 "Karen",52 "Leo",53 "Mia",54 "Noah",55 "Olivia",56 "Paul",57 "Quinn",58 "Ruby",59 "Sam",60 "Tina",61 ]62 const lastNames = [63 "Johnson",64 "Smith",65 "Williams",66 "Brown",67 "Martinez",68 "Lee",69 "Kim",70 "Davis",71 "Wilson",72 "Taylor",73 "Anderson",74 "Thomas",75 "Jackson",76 "White",77 "Harris",78 ]79 const departments = [80 "Engineering",81 "Design",82 "Marketing",83 "Sales",84 "Product",85 "HR",86 "Finance",87 "Operations",88 ]89 const roles = [90 "Senior Developer",91 "UI Designer",92 "Marketing Lead",93 "Backend Developer",94 "Product Manager",95 "DevOps Engineer",96 "UX Researcher",97 "Account Executive",98 "Data Analyst",99 "QA Engineer",100 ]101 const locations = [102 "New York",103 "San Francisco",104 "Chicago",105 "Austin",106 "Seattle",107 "Denver",108 "Portland",109 "Boston",110 "Miami",111 "Atlanta",112 ]113 const statuses: Employee["status"][] = ["active", "on-leave", "remote"]114
115 return Array.from({ length: count }, (_, i) => {116 const firstName = firstNames[i % firstNames.length]117 const lastName = lastNames[(i * 3) % lastNames.length]118 return {119 id: `EMP-${String(i + 1).padStart(4, "0")}`,120 name: `${firstName} ${lastName}`,121 email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${i}@company.com`,122 department: departments[i % departments.length],123 role: roles[(i * 5) % roles.length],124 location: locations[(i * 7) % locations.length],125 status: statuses[i % statuses.length],126 salary: 50000 + ((i * 1337) % 100000),127 }128 })129}130
131const data = generateEmployees(500)132
133// Status badge variant helper134const getStatusVariant = (status: Employee["status"]) => {135 switch (status) {136 case "active":137 return "default"138 case "remote":139 return "secondary"140 case "on-leave":141 return "outline"142 default:143 return "secondary"144 }145}146
147// Column definitions — all columns need explicit `id` for column ordering148const columns: DataTableColumnDef<Employee>[] = [149 {150 accessorKey: "id",151 id: "id",152 header: "Employee ID",153 size: 120,154 },155 {156 accessorKey: "name",157 id: "name",158 header: "Name",159 size: 170,160 cell: ({ row }) => (161 <div className="font-medium">{row.getValue("name")}</div>162 ),163 },164 {165 accessorKey: "email",166 id: "email",167 header: "Email",168 size: 250,169 cell: ({ row }) => (170 <div className="text-muted-foreground">{row.getValue("email")}</div>171 ),172 },173 {174 accessorKey: "department",175 id: "department",176 header: "Department",177 size: 140,178 },179 {180 accessorKey: "role",181 id: "role",182 header: "Role",183 size: 170,184 },185 {186 accessorKey: "location",187 id: "location",188 header: "Location",189 size: 140,190 },191 {192 accessorKey: "status",193 id: "status",194 header: "Status",195 size: 110,196 cell: ({ row }) => {197 const status = row.getValue("status") as Employee["status"]198 return <Badge variant={getStatusVariant(status)}>{status}</Badge>199 },200 },201 {202 accessorKey: "salary",203 id: "salary",204 header: "Salary",205 size: 120,206 cell: ({ row }) => {207 const salary = row.getValue("salary") as number208 return <div className="font-mono">${salary.toLocaleString()}</div>209 },210 },211]212
213const initialColumnOrder = columns.map(c => c.id as string)214
215export default function VirtualizedColumnDndExample() {216 const [columnOrder, setColumnOrder] =217 React.useState<string[]>(initialColumnOrder)218
219 return (220 <DataTableRoot221 data={data}222 columns={columns}223 state={{ columnOrder }}224 onColumnOrderChange={setColumnOrder}225 >226 <DataTableColumnResize />227 <DataTableColumnDndProvider228 columnOrder={columnOrder}229 onColumnOrderChange={setColumnOrder}230 >231 {/*232 * Two reorder surfaces, one state — header drag and view-menu drag233 * both update the same `columnOrder`. Search filters the 500-row234 * virtualized dataset on the fly.235 */}236 <DataTableToolbarSection className="justify-between">237 <DataTableSearchFilter placeholder="Search employees..." />238 <DataTableViewDndMenu239 columnOrder={columnOrder}240 onColumnOrderChange={setColumnOrder}241 onReset={() => setColumnOrder(initialColumnOrder)}242 />243 </DataTableToolbarSection>244 <DataTable height={500}>245 <DataTableVirtualizedDndHeader />246 <DataTableVirtualizedDndColumnBody estimateSize={40} overscan={10}>247 <DataTableVirtualizedEmptyBody>248 <DataTableEmptyMessage>249 <DataTableEmptyIcon>250 <Inbox className="size-12" />251 </DataTableEmptyIcon>252 <DataTableEmptyTitle>No employees found</DataTableEmptyTitle>253 <DataTableEmptyDescription>254 Try a different search.255 </DataTableEmptyDescription>256 </DataTableEmptyMessage>257 </DataTableVirtualizedEmptyBody>258 </DataTableVirtualizedDndColumnBody>259 </DataTable>260 </DataTableColumnDndProvider>261 </DataTableRoot>262 )263}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-ons. Add data-table-view-dnd-menu and data-table-search-filter for the toolbar shown in the example:
pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-column-dnd @niko-table/data-table-column-resize @niko-table/data-table-view-dnd-menu @niko-table/data-table-search-filter @niko-table/data-table-virtualized @niko-table/data-table-virtualized-column-dndFirst 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
1import {2 DataTableDndHeader,3 DataTableDndColumnBody,4} from "@/components/niko-table/core/data-table-column-dnd-structure"5
6const [columnOrder, setColumnOrder] = React.useState<string[]>(() =>7 columns.map(c => c.id as string),8)9
10return (11 <DataTableRoot12 data={data}13 columns={columns}14 state={{ columnOrder }}15 onColumnOrderChange={setColumnOrder}16 >17 <DataTableColumnResize />18 <DataTableColumnDndProvider19 columnOrder={columnOrder}20 onColumnOrderChange={setColumnOrder}21 >22 <DataTable>23 <DataTableDndHeader />24 <DataTableDndColumnBody />25 </DataTable>26 </DataTableColumnDndProvider>27 </DataTableRoot>28)Two Reorder Surfaces, One State
Section titled “Two Reorder Surfaces, One State”DataTableViewDndMenu is a drag-to-reorder variant of the View menu that shares the same columnOrder state as the column-header drag. Users get two ways to reorder, and visibility toggles + a Reset live in the same dropdown:
1<DataTableToolbarSection className="justify-between">2 <DataTableSearchFilter placeholder="Search employees..." />3 <DataTableViewDndMenu4 columnOrder={columnOrder}5 onColumnOrderChange={setColumnOrder}6 onReset={() => setColumnOrder(initialColumnOrder)}7 />8</DataTableToolbarSection>Use DataTableViewMenu (no DnD) instead if you don’t need drag-to-reorder — it skips the @dnd-kit/* bundle.
Key Points
Section titled “Key Points”- Every column needs an explicit
idfield for column order tracking columnOrderstate must be passed to bothDataTableRootandDataTableColumnDndProvider— andDataTableViewDndMenuif you use it- 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
DataTableViewDndMenuand the column-header drag share state, so both surfaces stay in lockstep
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