gridcn
exampledata-grid-column-layout-demosource: ok · hit
Data Grid Column Layout Demo
Column layout persistence: defaultColumnLayout seeds widths/order/pins/hidden from a localStorage snapshot at mount, onColumnLayoutChange writes every resize commit, reorder, pin, and hide back; a Reset button clears the snapshot and re-seeds.
npx shadcn add "https://gridcn.vercel.app/r/data-grid-column-layout-demo.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
DammersCode/gridcn/data-gridDammersCode/gridcn/data-grid-context-menuDammersCode/gridcn/data-grid-toolbar
Files2 · source cached 10/9/2026, 4:37:32 PM
registry/default/examples/data-grid-column-layout-demo.tsx
"use client";
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { Button } from "@/components/ui/button";
import {
DataGridProvider,
DataGridRoot,
DataGridHeader,
DataGridBody,
defineColumns,
type ColumnLayout,
} from "@/registry/default/components/data-grid/data-grid";
import {
DataGridContextMenu,
DataGridHeaderDropdown,
} from "@/registry/default/components/data-grid-context-menu/data-grid-context-menu";
import { DataGridToolbar, DataGridColumnsMenu } from "@/registry/default/components/data-grid-toolbar/data-grid-toolbar";
import { generateDemoRows, type DemoRow } from "./demo-data";
const STORAGE_KEY = "gridcn-column-layout-demo";
/** Reads the persisted layout from localStorage; safe on the server (returns undefined). */
function readLayout(): ColumnLayout | undefined {
if (typeof window === "undefined") return undefined;
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
return raw ? (JSON.parse(raw) as ColumnLayout) : undefined;
} catch {
return undefined;
}
}
const columns = defineColumns<DemoRow>()([
{ id: "name", header: "Name", accessorKey: "name", type: "text", width: 150, flex: 2 },
{
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);
/**
* Column layout persistence: `defaultColumnLayout` seeds widths/order/pins/hidden once at mount
* (NOT controlled — later changes are ignored), and `onColumnLayoutChange` fires from the store
* actions layer on every resize commit, reorder, pin, and hide. This demo round-trips the
* snapshot to localStorage, so a manual page reload restores the user's layout.
*/
export default function DataGridColumnLayoutDemo(): ReactNode {
const rows = useMemo(() => generateDemoRows(10), []);
// null = storage not read yet (SSR-safe gate: server and first client render show the
// placeholder, then the effect seeds the grid exactly once with the stored layout).
const [layout, setLayout] = useState<ColumnLayout | undefined | null>(null);
const [generation, setGeneration] = useState(0);
useEffect(() => {
setLayout(readLayout());
}, []);
const persist = useCallback((next: ColumnLayout) => {
setLayout(next);
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
} catch {
// storage unavailable (private mode) — the layout still lives in the store for this session
}
}, []);
const reset = () => {
try {
window.localStorage.removeItem(STORAGE_KEY);
} catch {
// ignore
}
setLayout(undefined);
setGeneration((g) => g + 1);
};
if (layout === null) {
return (
<div className="flex h-[340px] w-full items-center justify-center rounded-md border border-border text-sm text-muted-foreground">
Restoring saved layout…
</div>
);
}
return (
<div className="w-full flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
Resize a header edge, drag a header, or pin/hide a column (header menu / columns menu) —
every change persists and is restored on the next reload. Current order:{" "}
<span className="font-mono text-xs">{layout ? layout.order.join(" → ") : "(defaults)"}</span>
</p>
<div className="flex h-[400px] flex-col overflow-hidden rounded-md border border-border">
<DataGridProvider
key={generation}
defaultData={rows}
columns={columns}
getRowId={(row) => row.id}
defaultColumnLayout={layout}
onColumnLayoutChange={persist}
>
<DataGridContextMenu className="flex min-h-0 flex-1 flex-col">
<DataGridToolbar>
<DataGridColumnsMenu />
<div className="ml-auto">
<Button variant="outline" size="sm" onClick={reset}>
Reset layout
</Button>
</div>
</DataGridToolbar>
<DataGridRoot
className="min-h-0 flex-1 rounded-none border-none"
renderHeaderMenu={(ctx) => <DataGridHeaderDropdown {...ctx} />}
>
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridContextMenu>
</DataGridProvider>
</div>
</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;
}