How to Build a Dashboard in Next.js (Without Starting from Zero)
If you've built a SaaS product, you've built a dashboard. Sidebar, header, responsive layout, stats cards, data table, charts. Every team builds the same 80% — then spends three weeks debugging the sidebar collapse animation on mobile.
This guide covers the architecture decisions that matter and the ones that don't.
The Minimum Viable Dashboard
Before writing code, define what "done" looks like. Most dashboards need five things:
- Shell — sidebar navigation + header + main content area
- Stats overview — 3-5 KPI cards at the top
- Primary data view — usually a table or list
- Visualization — at least one chart showing trends
- Empty states — what new users see before they have data
Skip any of these and the dashboard feels incomplete. Over-engineer any of these and you waste time on things users don't notice.
Sidebar Architecture
The sidebar is deceptively complex. A production sidebar needs to handle:
- Responsive collapse — full sidebar on desktop, hamburger menu on mobile
- Active state — highlight the current route
- Grouped navigation — sections like "Overview," "Settings," "Team"
- Persistence — remember collapsed/expanded state across navigations
The architectural decision that matters most: should the sidebar be a server component or a client component?
Client component. The sidebar manages interactive state (open/close, active route, hover). But the layout that contains it should stay a server component — extract the sidebar into a client shell so the layout itself doesn't ship unnecessary JavaScript.
// layout.tsx — stays as a server component
export default function DashboardLayout({ children }) {
return <DashboardShell>{children}</DashboardShell>
}
// dashboard-shell.tsx — client component for interactivity
"use client"
export function DashboardShell({ children }) {
const [collapsed, setCollapsed] = useState(false)
return (
<div className="flex min-h-screen">
<Sidebar collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
<main className="flex-1">{children}</main>
</div>
)
}
Use CSS variables for sidebar colors (--sidebar-background, --sidebar-foreground). This keeps your dashboard theme independent from your marketing pages without conditional logic.
Data Tables That Don't Fall Apart
A data table isn't a styled <table> — it's a state machine managing sorting, filtering, pagination, column visibility, and row selection simultaneously.
The pattern that scales:
- Server component fetches data and passes it down
- Client component manages table interaction state
- URL search params store filter and sort state (so links are shareable)
Avoid fetching data inside the table component. The table should receive data as a prop, not own the data lifecycle. This keeps it reusable across different pages and data sources.
For the table engine, TanStack Table (formerly React Table) is the standard. It handles the state machine — you handle the rendering.
Charts Without Killing Your Bundle
Charting libraries are heavy. Recharts adds ~200KB. The mistake most teams make is importing the chart library at the top of the dashboard page, which means every user downloads it on first load — even before they scroll to the chart.
Two solutions:
- Dynamic imports — load the chart component only when needed
- Intersection Observer — load it when it scrolls into view
const AreaChart = dynamic(
() => import("@/components/dashboard/area-chart"),
{ loading: () => <div className="h-64 animate-pulse rounded-lg bg-muted" /> }
)
The dashboard shell renders instantly. The chart loads in the background.
Empty States Are Product Design
New users see an empty dashboard. If you show them a blank table and an empty chart, they bounce. Empty states need to:
- Explain what this section will show once there's data
- Provide a clear action to get started
- Feel intentional, not broken
This is a product decision, not a technical one. The best empty states look like a feature, not an error.
The Performance Checklist
Before shipping your dashboard:
- Initial load — does the shell render in under 1 second? (Heavy components should be lazy-loaded)
- Navigation — do page transitions feel instant? (Use Next.js prefetching and shared layouts)
- Data fetching — are you fetching on the server? (Client-side fetching adds a loading spinner to every page)
- Mobile — does the sidebar collapse work? (Test on a real phone, not just DevTools)
- Empty state — does a brand-new user see something useful?
The dashboard is the most-used surface in any SaaS product. Users interact with it daily. The patterns above aren't clever — they're what "production-ready" actually means.