zedkit

Build an admin dashboard

AppShell, DataTable, a drawer form, ConfirmDialog, and theme tokens in a small Next.js admin screen.

Hand-written tutorial. UI snippets live under prose/guides/snippets/ and are type-checked with the docs app (verify-admin-dashboard-tutorial.mjs in docs:check).

Who this is for

You build internal admin tools in React and want one screen that combines layout, data table, side-panel editing, and destructive confirmation — using the same components as the live docs demos.

Prerequisites

  • Node.js 22+, a Next.js App Router app, React 19, Tailwind CSS v4.
  • Theme wired first: follow Theme an admin app (.dark class + kit globals).
  • Keyboard: tab through the table actions; the drawer traps focus until closed.

Steps

1. Install the UI kit

pnpm add @zedtech/zedkit-ui

2. Add the dashboard view

Create app/admin-dashboard-page.tsx (or colocate with your route) with the composed screen:

Admin dashboard
"use client";

import {
  AppShell,
  Button,
  ConfirmDialog,
  DataTable,
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerTitle,
  DrawerTrigger,
  Field,
  FieldLabel,
  Input,
  PageHeader,
  Stack,
  Text,
  createDataTableColumns,
} from "@zedtech/zedkit-ui";

interface ItemRow {
  name: string;
  status: string;
}

const columnHelper = createDataTableColumns<ItemRow>();

const seed: ItemRow[] = [
  { name: "reports-api", status: "active" },
  { name: "billing-worker", status: "paused" },
];

export function AdminDashboardPage() {
  const columns = [
    columnHelper.accessor("name", { header: "Name" }),
    columnHelper.accessor("status", { header: "Status" }),
    columnHelper.display({
      id: "actions",
      header: "Actions",
      cell: ({ row }: { row: { original: ItemRow; id: string } }) => (
        <Stack direction="horizontal" gap={2}>
          <Drawer direction="right">
            <DrawerTrigger asChild>
              <Button variant="outline" size="sm">Edit</Button>
            </DrawerTrigger>
            <DrawerContent>
              <DrawerClose asChild>
                <Button variant="ghost" size="sm">Close</Button>
              </DrawerClose>
              <DrawerTitle>Edit {row.original.name}</DrawerTitle>
              <DrawerDescription>Update the display name. Focus returns here when you close the drawer.</DrawerDescription>
              <Field>
                <FieldLabel htmlFor={`name-${row.id}`}>Name</FieldLabel>
                <Input id={`name-${row.id}`} defaultValue={row.original.name} />
              </Field>
              <Button type="button">Save</Button>
            </DrawerContent>
          </Drawer>
          <ConfirmDialog
            trigger={<Button variant="destructive" size="sm">Delete</Button>}
            title={`Delete ${row.original.name}?`}
            description="This removes the row from the table in this demo."
            onConfirm={() => {}}
          />
        </Stack>
      ),
    }),
  ];

  return (
    <AppShell sidebar={<Text className="p-4 text-sm">Projects</Text>} topbar={<Text className="px-4 py-3 text-sm">Account</Text>}>
      <Stack gap={6} className="p-4 md:p-6">
        <PageHeader title="Items" description="Admin shell, sortable table, drawer form, and confirm dialog." />
        <DataTable columns={columns} data={seed} pageSize={10} />
      </Stack>
    </AppShell>
  );
}

Visible result: sidebar + top bar, a table with two rows, Edit opens a drawer with a name field, Delete opens a confirm dialog.

3. Mount the page

In app/page.tsx (or app/admin/page.tsx):

Route entry
import { AdminDashboardPage } from "./admin-dashboard-page";

export default function Page() {
  return <AdminDashboardPage />;
}

Run pnpm dev and open the route. Toggle light/dark using your theme control — tokens come from the theme step above.

Final result

One route renders AppShell, DataTable, Drawer + Field/Input, and ConfirmDialog without nested <button> inside <a>. Copy patterns from the catalog pages for Lay out an admin page, Show a sortable, filterable, paginated data table, Slide in a drawer panel, and Confirm destructive actions.

Troubleshooting

Unstyled controls — Kit CSS and Tailwind @source for @zedtech/zedkit-ui are missing. Re-run the theme guide; the table and buttons should use design tokens, not browser defaults.

Hydration warnings — Keep "use client" on the dashboard module (drawer and dialog use client state). The route file can stay a server component that imports the client view.

Focus lost after closing the drawer — Use DrawerClose (as in the snippet) so focus returns to the trigger; avoid unmounting the drawer with a bare conditional that skips the close animation.

Next steps

  • Add real data fetching in the route and pass rows as props into the view.
  • Replace the drawer field with your validation library; see Lay out fieldsets, fields, and error lists.
  • On mobile, the drawer goes full width — scroll the table horizontally only when columns overflow (min-w-0 on the shell content).