Build an admin dashboard
AppShell, DataTable, a drawer form, ConfirmDialog, and theme tokens in a small Next.js admin screen.
Every UI snippet on this page is type-checked exactly as written.
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-ui @fontsource-variable/inter @fontsource-variable/space-grotesk2. Add the dashboard view
Create app/admin-dashboard-page.tsx (or colocate with your route) with the composed screen:
"use client";
import {
AppShell,
Badge,
Button,
ConfirmDialog,
DataTable,
Drawer,
DrawerBody,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHandle,
DrawerHeader,
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" },
{ name: "webhooks", status: "active" },
{ name: "search-index", status: "paused" },
];
function Sidebar() {
return (
<Stack gap="lg" className="h-full surface-pad-compact">
<Stack gap="tight">
<Text className="font-semibold">Harbor Studio</Text>
<Text variant="muted" className="text-caption">
Admin
</Text>
</Stack>
<nav aria-label="Main">
<Stack gap="tight">
<Button type="button" variant="secondary" className="w-full justify-start" aria-current="page">
Projects
</Button>
<Button type="button" variant="ghost" className="w-full justify-start">
Services
</Button>
<Button type="button" variant="ghost" className="w-full justify-start">
Settings
</Button>
</Stack>
</nav>
</Stack>
);
}
function TopBar() {
return (
<Stack direction="horizontal" gap="sm" className="justify-end surface-pad-inline surface-pad-block-sm">
<Text
as="span"
aria-hidden="true"
className="flex size-person items-center justify-center rounded-full bg-primary text-caption font-semibold text-primary-foreground"
>
MC
</Text>
<Text as="span" className="text-dense font-medium">
Maya Chen
</Text>
</Stack>
);
}
export function AdminDashboardPage() {
const columns = [
columnHelper.accessor("name", { header: "Name" }),
columnHelper.accessor("status", {
header: "Status",
cell: ({ getValue }: { getValue: () => string }) => (
<Badge variant={getValue() === "active" ? "success" : "warning"}>{getValue()}</Badge>
),
}),
columnHelper.display({
id: "actions",
header: "Actions",
cell: ({ row }: { row: { original: ItemRow; id: string } }) => (
<Stack direction="horizontal" gap="sm">
<Drawer direction="right">
<DrawerTrigger asChild>
<Button variant="outline" size="sm">Edit</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHandle />
<DrawerHeader>
<DrawerTitle>Edit {row.original.name}</DrawerTitle>
<DrawerDescription>Update the display name. Focus returns here when you close the drawer.</DrawerDescription>
</DrawerHeader>
<DrawerBody>
<Field>
<FieldLabel htmlFor={`name-${row.id}`}>Name</FieldLabel>
<Input id={`name-${row.id}`} defaultValue={row.original.name} />
</Field>
</DrawerBody>
<DrawerFooter>
<Button type="button">Save</Button>
<DrawerClose asChild>
<Button type="button" variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</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={<Sidebar />} topbar={<TopBar />}>
<PageHeader title="Projects" description="Services in this workspace. Edit a row or remove it." />
<DataTable columns={columns} data={seed} pageSize={6} />
</AppShell>
);
}Visible result: brand and nav in the sidebar, account in the top bar, a table of services, 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 (
content-shrinkon the shell content).
Start without writing code
Design your look in the browser, hand the tokens to a coding agent or developer, review the result. No terminal for you.
Set up your coding agents
Install @zedtech/zedkit-ai, run init for your editor, validate the kit layout, and undo changes — independent from the UI package.