Next.js 14 Admin Dashboard with Tailwind CSS
A high-performance Admin Dashboard boilerplate built with Next.js 14 App Router, featuring a responsive sidebar, data tables, and Recharts analytics integration.
The Problem
Used by internal teams to monitor metrics, manage users, and process data. Dashboards are the backbone of B2B SaaS applications.
Real-World Use Case
Used by internal teams to monitor metrics, manage users, and process data. Dashboards are the backbone of B2B SaaS applications.
Technology Stack
Next.js App Router mechanics
Prerequisite
Tailwind CSS utility classes
Prerequisite
React Server Components vs Client Components
Prerequisite
Architecture & Design
Folder Structure
src/
├── app/
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── globals.css
├── components/
│ ├── Sidebar.tsx
│ ├── TopNav.tsx
│ └── MetricCard.tsxAPI Design
APIUtilizes Next.js Server Actions for secure, API-less data fetching directly from the database.
Step-by-Step Implementation
Set up Next.js 14 App Router with Tailwind.
### Step 1: Project Initialization Run `npx create-next-app@latest my-dashboard`. Select TypeScript, Tailwind CSS, and App Router. Install Lucide Icons for UI elements (`npm i lucide-react`).
// Full source code is split across components in a real Next.js app.
// Here is a consolidated visualization of the Dashboard Page (page.tsx)
import { MetricCard } from '@/components/MetricCard';
import { Users, DollarSign, Activity, ShoppingCart } from 'lucide-react';
export default async function DashboardPage() {
// Simulate server-side data fetching
// const metrics = await db.query('SELECT ...')
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard Overview</h1>
<button className="px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded-lg shadow-sm">
Download Report
</button>
</div>
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Total Revenue</h3>
<DollarSign className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">$45,231.89</div>
<p className="text-xs text-emerald-600 mt-1">+20.1% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Users</h3>
<Users className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+2350</div>
<p className="text-xs text-emerald-600 mt-1">+180 new today</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Sales</h3>
<ShoppingCart className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+12,234</div>
<p className="text-xs text-rose-600 mt-1">-4% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Now</h3>
<Activity className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+573</div>
<p className="text-xs text-emerald-600 mt-1">+201 since last hour</p>
</div>
</div>
{/* Chart Section Placeholder */}
<div className="grid gap-6 grid-cols-1 lg:grid-cols-7">
<div className="col-span-1 lg:col-span-4 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recharts Line Chart Goes Here</p>
</div>
<div className="col-span-1 lg:col-span-3 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recent Activity List Goes Here</p>
</div>
</div>
</div>
);
}Code Explanation
Implementation step
Create a (dashboard) route group to share a common layout.
### Step 2: The Dashboard Layout Create `app/(dashboard)/layout.tsx`. This file ensures the Sidebar and TopNav persist across all dashboard pages without re-rendering. ```tsx import Sidebar from '@/components/Sidebar'; import TopNav from '@/components/TopNav'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <div className="flex h-screen bg-slate-50"> <Sidebar /> <div className="flex-1 flex flex-col overflow-hidden"> <TopNav /> <main className="flex-1 overflow-y-auto p-6"> {children} </main> </div> </div> ); } ```
// Full source code is split across components in a real Next.js app.
// Here is a consolidated visualization of the Dashboard Page (page.tsx)
import { MetricCard } from '@/components/MetricCard';
import { Users, DollarSign, Activity, ShoppingCart } from 'lucide-react';
export default async function DashboardPage() {
// Simulate server-side data fetching
// const metrics = await db.query('SELECT ...')
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard Overview</h1>
<button className="px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded-lg shadow-sm">
Download Report
</button>
</div>
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Total Revenue</h3>
<DollarSign className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">$45,231.89</div>
<p className="text-xs text-emerald-600 mt-1">+20.1% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Users</h3>
<Users className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+2350</div>
<p className="text-xs text-emerald-600 mt-1">+180 new today</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Sales</h3>
<ShoppingCart className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+12,234</div>
<p className="text-xs text-rose-600 mt-1">-4% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Now</h3>
<Activity className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+573</div>
<p className="text-xs text-emerald-600 mt-1">+201 since last hour</p>
</div>
</div>
{/* Chart Section Placeholder */}
<div className="grid gap-6 grid-cols-1 lg:grid-cols-7">
<div className="col-span-1 lg:col-span-4 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recharts Line Chart Goes Here</p>
</div>
<div className="col-span-1 lg:col-span-3 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recent Activity List Goes Here</p>
</div>
</div>
</div>
);
}Code Explanation
Implementation step
Build the Sidebar component with navigational links.
### Step 3: Creating Metric Cards Create reusable UI components to display key performance indicators. ```tsx export function MetricCard({ title, value, trend }: { title: string, value: string, trend: string }) { return ( <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm"> <h3 className="text-sm font-semibold text-slate-500">{title}</h3> <div className="mt-2 flex items-baseline gap-2"> <span className="text-3xl font-bold text-slate-900">{value}</span> <span className="text-sm font-medium text-emerald-600">{trend}</span> </div> </div> ); } ```
// Full source code is split across components in a real Next.js app.
// Here is a consolidated visualization of the Dashboard Page (page.tsx)
import { MetricCard } from '@/components/MetricCard';
import { Users, DollarSign, Activity, ShoppingCart } from 'lucide-react';
export default async function DashboardPage() {
// Simulate server-side data fetching
// const metrics = await db.query('SELECT ...')
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard Overview</h1>
<button className="px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded-lg shadow-sm">
Download Report
</button>
</div>
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Total Revenue</h3>
<DollarSign className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">$45,231.89</div>
<p className="text-xs text-emerald-600 mt-1">+20.1% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Users</h3>
<Users className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+2350</div>
<p className="text-xs text-emerald-600 mt-1">+180 new today</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Sales</h3>
<ShoppingCart className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+12,234</div>
<p className="text-xs text-rose-600 mt-1">-4% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Now</h3>
<Activity className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+573</div>
<p className="text-xs text-emerald-600 mt-1">+201 since last hour</p>
</div>
</div>
{/* Chart Section Placeholder */}
<div className="grid gap-6 grid-cols-1 lg:grid-cols-7">
<div className="col-span-1 lg:col-span-4 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recharts Line Chart Goes Here</p>
</div>
<div className="col-span-1 lg:col-span-3 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recent Activity List Goes Here</p>
</div>
</div>
</div>
);
}Code Explanation
Implementation step
Build the TopNav component with a user avatar and search bar.
### Step 4: Loading States Add a `loading.tsx` file inside the (dashboard) directory. Next.js will automatically display this skeleton UI while data is being fetched on the server.
// Full source code is split across components in a real Next.js app.
// Here is a consolidated visualization of the Dashboard Page (page.tsx)
import { MetricCard } from '@/components/MetricCard';
import { Users, DollarSign, Activity, ShoppingCart } from 'lucide-react';
export default async function DashboardPage() {
// Simulate server-side data fetching
// const metrics = await db.query('SELECT ...')
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard Overview</h1>
<button className="px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded-lg shadow-sm">
Download Report
</button>
</div>
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Total Revenue</h3>
<DollarSign className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">$45,231.89</div>
<p className="text-xs text-emerald-600 mt-1">+20.1% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Users</h3>
<Users className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+2350</div>
<p className="text-xs text-emerald-600 mt-1">+180 new today</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Sales</h3>
<ShoppingCart className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+12,234</div>
<p className="text-xs text-rose-600 mt-1">-4% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Now</h3>
<Activity className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+573</div>
<p className="text-xs text-emerald-600 mt-1">+201 since last hour</p>
</div>
</div>
{/* Chart Section Placeholder */}
<div className="grid gap-6 grid-cols-1 lg:grid-cols-7">
<div className="col-span-1 lg:col-span-4 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recharts Line Chart Goes Here</p>
</div>
<div className="col-span-1 lg:col-span-3 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recent Activity List Goes Here</p>
</div>
</div>
</div>
);
}Code Explanation
Implementation step
Construct the main dashboard page using CSS Grid to organize MetricCards.
### Step 4: Loading States Add a `loading.tsx` file inside the (dashboard) directory. Next.js will automatically display this skeleton UI while data is being fetched on the server.
// Full source code is split across components in a real Next.js app.
// Here is a consolidated visualization of the Dashboard Page (page.tsx)
import { MetricCard } from '@/components/MetricCard';
import { Users, DollarSign, Activity, ShoppingCart } from 'lucide-react';
export default async function DashboardPage() {
// Simulate server-side data fetching
// const metrics = await db.query('SELECT ...')
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard Overview</h1>
<button className="px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded-lg shadow-sm">
Download Report
</button>
</div>
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Total Revenue</h3>
<DollarSign className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">$45,231.89</div>
<p className="text-xs text-emerald-600 mt-1">+20.1% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Users</h3>
<Users className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+2350</div>
<p className="text-xs text-emerald-600 mt-1">+180 new today</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Sales</h3>
<ShoppingCart className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+12,234</div>
<p className="text-xs text-rose-600 mt-1">-4% from last month</p>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-semibold text-slate-500">Active Now</h3>
<Activity className="h-4 w-4 text-slate-400" />
</div>
<div className="text-3xl font-bold">+573</div>
<p className="text-xs text-emerald-600 mt-1">+201 since last hour</p>
</div>
</div>
{/* Chart Section Placeholder */}
<div className="grid gap-6 grid-cols-1 lg:grid-cols-7">
<div className="col-span-1 lg:col-span-4 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recharts Line Chart Goes Here</p>
</div>
<div className="col-span-1 lg:col-span-3 bg-white p-6 rounded-2xl border border-slate-200 shadow-sm h-96 flex items-center justify-center">
<p className="text-slate-400 font-medium">Recent Activity List Goes Here</p>
</div>
</div>
</div>
);
}Code Explanation
Implementation step
Common Errors
If using Recharts, ensure the chart component has 'use client' at the top of the file.
Ensure the layout is in layout.tsx, not imported into individual pages.
Security & Performance
Resize browser window to verify the grid collapses to single columns on mobile devices.
Verify the Sidebar toggles open/closed correctly on mobile viewports.
Simulate a slow network connection to ensure loading.tsx skeleton appears.
Add Dark Mode toggling using next-themes.
Integrate next-intl for multi-language support on the dashboard.
Export charts to PDF/Image.
Interview Questions
Q: Should my Dashboard components be Server or Client components?
A: Keep layouts and data-fetching pages as Server Components. Only use Client Components ('use client') for interactive UI elements like Charts, Modals, and the Sidebar toggle.