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 (
.darkclass + kit globals). - Keyboard: tab through the table actions; the drawer traps focus until closed.
Steps
1. Install the UI kit
pnpm add @zedtech/zedkit-ui2. Add the dashboard view
Create app/admin-dashboard-page.tsx (or colocate with your route) with the composed screen:
"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):
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-0on the shell content).