shadimage
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 · light
iconslucide-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;
}

More from gridcn

Browse all components in this library →