gridcn
exampledata-grid-column-drag-demosource: ok · hit
Data Grid Column Drag Demo
Drag a header to reorder columns within its pin zone (enableColumnReorder, per-column reorderable opt-out, pinned-right column), plus a column picker wired to the programmatic setColumnOrder(id, targetId, position) path.
npx shadcn add "https://gridcn.vercel.app/r/data-grid-column-drag-demo.json"free
Where it goes
navbar
Interaction
drag
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
DammersCode/gridcn/data-grid
Files2 · source cached 10/9/2026, 11:51:55 PM
registry/default/examples/data-grid-column-drag-demo.tsx
"use client";
import { useMemo, useState, type ReactNode } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronLeft, ChevronRight } from "lucide-react";
import {
DataGridProvider,
DataGridRoot,
DataGridHeader,
DataGridBody,
defineColumns,
useDataGridActions,
useDataGridVisibleColumns,
} from "@/registry/default/components/data-grid/data-grid";
import { generateDemoRows, type DemoRow } from "./demo-data";
const columns = defineColumns<DemoRow>()([
{ id: "name", header: "Name", accessorKey: "name", type: "text", width: 150, flex: 2, reorderable: false },
{
id: "role",
header: "Role",
accessorKey: "role",
type: "select",
options: {
choices: [
{ value: "Admin", label: "Admin" },
{ value: "User", label: "User" },
{ value: "Editor", label: "Editor" },
{ value: "Viewer", label: "Viewer" },
{ value: "Manager", label: "Manager" },
],
},
width: 110,
flex: 1,
},
{ id: "age", header: "Age", accessorKey: "age", type: "number", options: { min: 18, max: 100 }, width: 80, flex: 1 },
{ id: "joined", header: "Joined", accessorKey: "joined", type: "date", width: 110, flex: 1 },
{ id: "score", header: "Score", accessorKey: "score", type: "number", options: { min: 0, max: 100 }, width: 80, flex: 1, pin: "right" },
] as const);
/** Pick a column + move it one step left/right through the programmatic `setColumnOrder` path. */
function ReorderControls(): ReactNode {
const actions = useDataGridActions();
const visible = useDataGridVisibleColumns<DemoRow>();
const [selectedId, setSelectedId] = useState<string | null>(() => visible[1]?.id ?? visible[0]?.id ?? null);
const index = selectedId === null ? -1 : visible.findIndex((column) => column.id === selectedId);
const move = (step: -1 | 1) => {
if (selectedId === null) return;
const target = visible[index + step];
if (target === undefined) return;
actions.setColumnOrder(selectedId, target.id, step === -1 ? "before" : "after");
};
if (visible.length < 2) return null;
return (
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">Move</span>
<Select value={selectedId} onValueChange={setSelectedId}>
<SelectTrigger className="h-8 w-40" aria-label="Column to move">
<SelectValue />
</SelectTrigger>
<SelectContent>
{visible.map((column) => (
<SelectItem key={column.id} value={column.id}>
{column.header}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="sm" disabled={index <= 0} onClick={() => move(-1)}>
<ChevronLeft className="size-4" /> Left
</Button>
<Button variant="outline" size="sm" disabled={index < 0 || index >= visible.length - 1} onClick={() => move(1)}>
Right <ChevronRight className="size-4" />
</Button>
</div>
);
}
/**
* Column drag & drop: `enableColumnReorder` lets a header drag within its pin zone (Name opts
* out via `reorderable: false`, Score is pinned right and moves only with the pinned zone), and
* the picker + buttons show the programmatic `setColumnOrder(id, targetId, position)` path.
*/
export default function DataGridColumnDragDemo(): ReactNode {
const rows = useMemo(() => generateDemoRows(10), []);
return (
<div className="w-full flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
Drag a header to reorder it — the move stays inside its pin zone. Name cannot be moved
(<code>reorderable: false</code>) and Score is pinned right. The picker below the line
moves the chosen column programmatically.
</p>
<DataGridProvider defaultData={rows} columns={columns} getRowId={(row) => row.id} enableColumnReorder>
<ReorderControls />
<div className="h-[340px] overflow-hidden rounded-md border border-border">
<DataGridRoot className="h-full rounded-none border-none">
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</div>
</DataGridProvider>
</div>
);
}
registry/default/examples/demo-data.ts
/** Deterministic LCG so SSR/CSR markup match without Math.random(); shared across example rows. */
function createSeededLcg(seed: number) {
let state = seed;
return () => {
state = (state * 1103515245 + 12345) & 0x7fffffff;
return state / 0x7fffffff;
};
}
export type DemoRow = {
id: string;
name: string;
email: string;
age: number;
active: boolean;
role: string;
joined: string;
score: number;
};
const ROLES = ["Admin", "User", "Editor", "Viewer", "Manager"] as const;
const ADJECTIVES = ["Eager", "Quick", "Silent", "Bold", "Swift"] as const;
const NOUNS = ["Eagle", "Lion", "Tiger", "Bear", "Wolf"] as const;
/** Generates `count` deterministic demo rows for example previews (seed 42, stable across renders). */
export function generateDemoRows(count: number, seed = 42): DemoRow[] {
const next = createSeededLcg(seed);
const rows: DemoRow[] = [];
for (let i = 0; i < count; i++) {
rows.push({
id: `row-${i}`,
name: `${ADJECTIVES[Math.floor(next() * ADJECTIVES.length)]} ${NOUNS[Math.floor(next() * NOUNS.length)]}`,
email: `user${i}@example.com`,
age: Math.floor(next() * 50) + 18,
active: next() > 0.5,
role: ROLES[Math.floor(next() * ROLES.length)]!, // ROLES is a fixed non-empty literal array
joined: `${2020 + Math.floor(next() * 5)}-${String(Math.floor(next() * 12) + 1).padStart(2, "0")}-${String(Math.floor(next() * 28) + 1).padStart(2, "0")}`,
score: Math.floor(next() * 100),
});
}
return rows;
}