Skip to main content
Back to Blog
Tutorials
3 min read
November 13, 2024

How to Build a Dashboard Layout in Next.js

Create a responsive dashboard layout with collapsible sidebar, header, breadcrumbs, and nested routes using Next.js App Router.

Ryel Banfield

Founder & Lead Developer

A dashboard layout needs a sidebar for navigation, a header, and a main content area. Here is how to build one that collapses on mobile and persists across routes.

Step 1: Route Structure

app/
  dashboard/
    layout.tsx        ← Dashboard shell
    page.tsx          ← Dashboard home
    loading.tsx       ← Loading state
    analytics/
      page.tsx
    settings/
      page.tsx
    customers/
      page.tsx
      [id]/
        page.tsx

Step 2: Define Navigation Items

// lib/dashboard-nav.ts
export type NavItem = {
  label: string;
  href: string;
  icon: string;
  badge?: number;
};

export const navItems: NavItem[] = [
  { label: "Dashboard", href: "/dashboard", icon: "home" },
  { label: "Analytics", href: "/dashboard/analytics", icon: "chart" },
  { label: "Customers", href: "/dashboard/customers", icon: "users", badge: 12 },
  { label: "Orders", href: "/dashboard/orders", icon: "package" },
  { label: "Products", href: "/dashboard/products", icon: "box" },
  { label: "Settings", href: "/dashboard/settings", icon: "settings" },
];

Step 3: Build the Sidebar

"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems } from "@/lib/dashboard-nav";
import { cn } from "@/lib/utils";

export function Sidebar({
  collapsed,
  onClose,
}: {
  collapsed: boolean;
  onClose?: () => void;
}) {
  const pathname = usePathname();

  return (
    <aside
      className={cn(
        "flex h-full flex-col border-r bg-white dark:border-gray-800 dark:bg-gray-950",
        collapsed ? "w-16" : "w-64"
      )}
    >
      {/* Logo */}
      <div className="flex h-16 items-center border-b px-4 dark:border-gray-800">
        <Link href="/dashboard" className="flex items-center gap-2">
          <div className="h-8 w-8 rounded-lg bg-blue-600" />
          {!collapsed && (
            <span className="text-lg font-bold">Dashboard</span>
          )}
        </Link>
      </div>

      {/* Navigation */}
      <nav className="flex-1 overflow-y-auto p-3">
        <ul className="space-y-1">
          {navItems.map((item) => {
            const isActive =
              item.href === "/dashboard"
                ? pathname === "/dashboard"
                : pathname.startsWith(item.href);

            return (
              <li key={item.href}>
                <Link
                  href={item.href}
                  onClick={onClose}
                  className={cn(
                    "flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
                    isActive
                      ? "bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400"
                      : "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
                  )}
                >
                  <NavIcon name={item.icon} className="h-5 w-5 flex-shrink-0" />
                  {!collapsed && (
                    <>
                      <span className="flex-1">{item.label}</span>
                      {item.badge && (
                        <span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-600 dark:bg-blue-900/30">
                          {item.badge}
                        </span>
                      )}
                    </>
                  )}
                </Link>
              </li>
            );
          })}
        </ul>
      </nav>

      {/* User */}
      <div className="border-t p-3 dark:border-gray-800">
        <div className="flex items-center gap-3 rounded-lg px-3 py-2">
          <div className="h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700" />
          {!collapsed && (
            <div className="flex-1 truncate">
              <p className="text-sm font-medium">John Doe</p>
              <p className="text-xs text-gray-500">john@example.com</p>
            </div>
          )}
        </div>
      </div>
    </aside>
  );
}

Step 4: Build the Header

"use client";

export function DashboardHeader({
  onMenuToggle,
}: {
  onMenuToggle: () => void;
}) {
  return (
    <header className="flex h-16 items-center justify-between border-b px-4 dark:border-gray-800 lg:px-6">
      {/* Mobile menu button */}
      <button
        onClick={onMenuToggle}
        className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800 lg:hidden"
        aria-label="Toggle menu"
      >
        <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
          <path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
        </svg>
      </button>

      {/* Search */}
      <div className="flex-1 px-4">
        <div className="relative max-w-md">
          <svg className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
          </svg>
          <input
            type="text"
            placeholder="Search..."
            className="w-full rounded-lg border bg-gray-50 py-2 pl-10 pr-4 text-sm dark:border-gray-700 dark:bg-gray-800"
          />
        </div>
      </div>

      {/* Actions */}
      <div className="flex items-center gap-2">
        <button className="relative rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800">
          <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
          </svg>
          <span className="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-red-500" />
        </button>
      </div>
    </header>
  );
}

Step 5: Dashboard Layout

// app/dashboard/layout.tsx
"use client";

import { useState } from "react";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { DashboardHeader } from "@/components/dashboard/Header";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [collapsed, setCollapsed] = useState(false);

  return (
    <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
      {/* Desktop sidebar */}
      <div className="hidden lg:block">
        <Sidebar collapsed={collapsed} />
      </div>

      {/* Mobile sidebar overlay */}
      {sidebarOpen && (
        <div className="fixed inset-0 z-40 lg:hidden">
          <div
            className="absolute inset-0 bg-black/50"
            onClick={() => setSidebarOpen(false)}
          />
          <div className="relative z-50 h-full w-64">
            <Sidebar collapsed={false} onClose={() => setSidebarOpen(false)} />
          </div>
        </div>
      )}

      {/* Main content */}
      <div className="flex flex-1 flex-col overflow-hidden">
        <DashboardHeader onMenuToggle={() => setSidebarOpen(!sidebarOpen)} />
        <main className="flex-1 overflow-y-auto p-4 lg:p-6">{children}</main>
      </div>
    </div>
  );
}

Step 6: Dashboard Home Page

// app/dashboard/page.tsx
export default function DashboardHome() {
  return (
    <div>
      <h1 className="text-2xl font-bold">Dashboard</h1>

      {/* Stats grid */}
      <div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {[
          { label: "Total Revenue", value: "$45,231", change: "+12%" },
          { label: "New Customers", value: "2,350", change: "+8%" },
          { label: "Active Projects", value: "12", change: "+2" },
          { label: "Conversion Rate", value: "3.2%", change: "+0.5%" },
        ].map((stat) => (
          <div
            key={stat.label}
            className="rounded-lg border bg-white p-6 dark:border-gray-800 dark:bg-gray-950"
          >
            <p className="text-sm text-gray-500">{stat.label}</p>
            <p className="mt-1 text-2xl font-bold">{stat.value}</p>
            <p className="mt-1 text-sm text-green-600">{stat.change}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Responsive Behavior

  • Desktop (lg+): Fixed sidebar + scrollable content area
  • Tablet: Collapsed sidebar (icons only) or hidden with toggle
  • Mobile: Sidebar hidden by default, opens as overlay when menu button is tapped

Need a Custom Dashboard?

We build custom dashboards and admin interfaces for SaaS products and business tools. Contact us for a consultation.

dashboardlayoutNext.jssidebartutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles