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

More from gridcn

Browse all components in this library →