gridcn
exampledata-grid-actions-demosource: ok · hit
Data Grid Actions Demo
Action-buttons column pattern: a pinned-right readOnly column whose renderCell composes shadcn Buttons over store actions (startEditing on the row's first editable cell, deleteRows/duplicateRows by view index) plus a more dropdown (Edit, Duplicate, Delete).
npx shadcn add "https://gridcn.vercel.app/r/data-grid-actions-demo.json"free
Where it goes
data-table
Interaction
dropdown
Use cases
dashboard
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/10/2026, 12:10:37 AM
registry/default/examples/data-grid-actions-demo.tsx
"use client";
import { useMemo, type ReactNode } from "react";
import { Pencil, Trash2, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
DataGridProvider,
DataGridRoot,
DataGridHeader,
DataGridBody,
defineColumns,
useDataGridActions,
} from "@/registry/default/components/data-grid/data-grid";
import { generateDemoRows, type DemoRow } from "./demo-data";
function RowActions({ rowIndex }: { rowIndex: number }): ReactNode {
const actions = useDataGridActions();
// The row's first editable cell (Name, col 0) is where Edit drops the user.
const editRow = () => actions.startEditing({ row: rowIndex, col: 0 });
return (
<div className="flex items-center justify-end gap-1">
<Button
size="icon"
variant="ghost"
className="size-7"
aria-label="Edit row"
onClick={editRow}
>
<Pencil className="size-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="size-7"
aria-label="Delete row"
onClick={() => actions.deleteRows([rowIndex])}
>
<Trash2 className="size-3.5" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="icon"
variant="ghost"
className="size-7"
aria-label="More actions"
>
<MoreHorizontal className="size-3.5" />
</Button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={editRow}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => actions.duplicateRows([rowIndex])}>
Duplicate
</DropdownMenuItem>
<DropdownMenuItem onClick={() => actions.deleteRows([rowIndex])}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
const columns = defineColumns<DemoRow>()([
{
id: "name",
header: "Name",
accessorKey: "name",
type: "text",
width: 140,
flex: 2,
},
{
id: "role",
header: "Role",
accessorKey: "role",
type: "select",
width: 90,
flex: 1,
},
{
id: "age",
header: "Age",
accessorKey: "age",
type: "number",
width: 70,
flex: 1,
},
{
id: "actions",
header: "",
// accessorFn (no accessorKey) keeps the column readable but read-only by construction; the
// value is unused — renderCell renders the buttons instead.
accessorFn: () => "",
pin: "right",
readOnly: true,
width: 110,
renderCell: ({ rowIndex }) => <RowActions rowIndex={rowIndex} />,
},
] as const);
/**
* The action-buttons column pattern: a pinned-right `readOnly` column that composes shadcn
* Buttons over store actions (`startEditing` drops the row's first editable cell into edit mode,
* `deleteRows`/`duplicateRows` work on the row by its view index). No new core feature — pure
* `renderCell` composition.
*/
export default function DataGridActionsDemo(): ReactNode {
const rows = useMemo(() => generateDemoRows(6), []);
return (
<div className="w-full flex flex-col gap-3">
<p className="text-sm text-muted-foreground">
Each row's pinned-right column offers inline Edit / Delete buttons plus
a "more" dropdown (Edit, Duplicate, Delete) — all plain store actions.
</p>
<div className="h-[260px] overflow-hidden rounded-md border border-border">
<DataGridProvider
defaultData={rows}
columns={columns}
getRowId={(row) => row.id}
>
<DataGridRoot className="h-full rounded-none border-none">
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</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;
}