shadcn dashboard UI
Every dashboard view is composed from shadcn/ui components — generated
into components/ui/ so the code is yours to edit, not a locked dependency.
Reach for an existing component before writing bespoke UI; restyle by editing
the generated file or the theme tokens, never by fighting it with overrides.
Setup
npx shadcn@latest init
npx shadcn@latest add sidebar button card table dropdown-menu avatar \
input badge separator sheet skeleton
init writes components.json and the CSS variables theme into
app/globals.css. Add further components on demand with
npx shadcn@latest add <name> — don't pre-install the whole catalog.
The dashboard shell
Build the protected layout from the sidebar primitives:
// app/dashboard/layout.tsx
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar";
import { UserButton } from "@clerk/nextjs";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-14 items-center gap-2 border-b px-4">
<SidebarTrigger />
<div className="ml-auto"><UserButton /></div>
</header>
<main className="flex-1 p-6">{children}</main>
</SidebarInset>
</SidebarProvider>
);
}
AppSidebar is your own component built from Sidebar, SidebarContent,
SidebarMenu, and SidebarMenuButton — one menu item per dashboard section,
with the active route highlighted via usePathname().
View patterns
- Overview page: a responsive grid of
Cardstat tiles (grid gap-4 md:grid-cols-2 lg:grid-cols-4), followed by a recent-activityTable. Fetch data in the Server Component and pass plain props down. - List views:
Tableinside aCard, with a toolbar row (searchInput, filterDropdownMenu, primary-actionButton) above it. Reach for TanStack Table only when you actually need client-side sorting/selection. - Detail/edit:
Sheetor a dedicated route — prefer a route when the view is deep enough to deserve a URL. - Loading: mirror each view's layout with
Skeletonblocks inloading.tsxso navigation feels instant.
Conventions
- Keep generated primitives pristine in
components/ui/; compose them into app-level pieces (app-sidebar.tsx,stat-card.tsx) one directory up. - Theme through the CSS variables in
globals.css— change tokens once rather than sprinkling color utilities per component. - Dashboards are data-dense: default to compact spacing,
text-smtables, and restrained color (letBadgevariants carry status, not full rows).