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

How to Build a Sidebar Navigation Component in React

Build a collapsible sidebar navigation with nested items, icons, badges, and responsive behavior in React.

Ryel Banfield

Founder & Lead Developer

A collapsible sidebar is the standard navigation pattern for dashboards and admin panels. Here is how to build one.

Step 1: Navigation Data Structure

// lib/navigation.ts
import {
  LayoutDashboard,
  Users,
  Package,
  FileText,
  Settings,
  BarChart,
  Mail,
  HelpCircle,
  type LucideIcon,
} from "lucide-react";

export interface NavItem {
  label: string;
  href: string;
  icon: LucideIcon;
  badge?: string;
  children?: NavItem[];
}

export const navigation: NavItem[] = [
  { label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
  {
    label: "Users",
    href: "/users",
    icon: Users,
    badge: "12",
    children: [
      { label: "All Users", href: "/users", icon: Users },
      { label: "Roles", href: "/users/roles", icon: Users },
      { label: "Permissions", href: "/users/permissions", icon: Users },
    ],
  },
  { label: "Products", href: "/products", icon: Package },
  {
    label: "Content",
    href: "/content",
    icon: FileText,
    children: [
      { label: "Blog Posts", href: "/content/posts", icon: FileText },
      { label: "Pages", href: "/content/pages", icon: FileText },
      { label: "Media", href: "/content/media", icon: FileText },
    ],
  },
  { label: "Analytics", href: "/analytics", icon: BarChart },
  { label: "Messages", href: "/messages", icon: Mail, badge: "3" },
  { label: "Settings", href: "/settings", icon: Settings },
  { label: "Help", href: "/help", icon: HelpCircle },
];

Step 2: Sidebar Component

"use client";

import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  PanelLeftClose,
  PanelLeft,
} from "lucide-react";
import { navigation, type NavItem } from "@/lib/navigation";

export function Sidebar() {
  const [isCollapsed, setIsCollapsed] = useState(false);
  const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
  const pathname = usePathname();

  function toggleExpand(label: string) {
    setExpandedItems((prev) => {
      const next = new Set(prev);
      if (next.has(label)) next.delete(label);
      else next.add(label);
      return next;
    });
  }

  return (
    <aside
      className={`flex h-screen flex-col border-r bg-white transition-all dark:border-gray-800 dark:bg-gray-900 ${
        isCollapsed ? "w-16" : "w-64"
      }`}
    >
      {/* Header */}
      <div className="flex h-14 items-center justify-between border-b px-3 dark:border-gray-800">
        {!isCollapsed && (
          <span className="text-lg font-bold">AppName</span>
        )}
        <button
          onClick={() => setIsCollapsed(!isCollapsed)}
          className="rounded-lg p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
        >
          {isCollapsed ? (
            <PanelLeft className="h-5 w-5" />
          ) : (
            <PanelLeftClose className="h-5 w-5" />
          )}
        </button>
      </div>

      {/* Navigation */}
      <nav className="flex-1 overflow-y-auto p-2">
        <ul className="space-y-1">
          {navigation.map((item) => (
            <SidebarItem
              key={item.label}
              item={item}
              isCollapsed={isCollapsed}
              isActive={pathname === item.href || pathname.startsWith(item.href + "/")}
              isExpanded={expandedItems.has(item.label)}
              onToggle={() => toggleExpand(item.label)}
              pathname={pathname}
            />
          ))}
        </ul>
      </nav>

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

function SidebarItem({
  item,
  isCollapsed,
  isActive,
  isExpanded,
  onToggle,
  pathname,
}: {
  item: NavItem;
  isCollapsed: boolean;
  isActive: boolean;
  isExpanded: boolean;
  onToggle: () => void;
  pathname: string;
}) {
  const hasChildren = item.children && item.children.length > 0;
  const Icon = item.icon;

  if (isCollapsed) {
    return (
      <li>
        <Link
          href={item.href}
          className={`flex items-center justify-center rounded-lg p-2 transition-colors ${
            isActive
              ? "bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
              : "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
          }`}
          title={item.label}
        >
          <Icon className="h-5 w-5" />
          {item.badge && (
            <span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] text-white">
              {item.badge}
            </span>
          )}
        </Link>
      </li>
    );
  }

  return (
    <li>
      <div className="flex items-center">
        <Link
          href={item.href}
          className={`flex flex-1 items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
            isActive && !hasChildren
              ? "bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
              : "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
          }`}
        >
          <Icon className="h-4 w-4 shrink-0" />
          <span className="flex-1">{item.label}</span>
          {item.badge && (
            <span className="rounded-full bg-red-100 px-1.5 py-0.5 text-[10px] font-medium text-red-700 dark:bg-red-900 dark:text-red-300">
              {item.badge}
            </span>
          )}
        </Link>
        {hasChildren && (
          <button
            onClick={onToggle}
            className="rounded p-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
          >
            <ChevronDown
              className={`h-4 w-4 transition-transform ${
                isExpanded ? "rotate-180" : ""
              }`}
            />
          </button>
        )}
      </div>
      {hasChildren && isExpanded && (
        <ul className="ml-4 mt-1 space-y-1 border-l pl-3 dark:border-gray-700">
          {item.children!.map((child) => (
            <li key={child.href}>
              <Link
                href={child.href}
                className={`block rounded-lg px-3 py-1.5 text-sm transition-colors ${
                  pathname === child.href
                    ? "text-blue-700 dark:text-blue-300"
                    : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
                }`}
              >
                {child.label}
              </Link>
            </li>
          ))}
        </ul>
      )}
    </li>
  );
}

Step 3: Mobile Sidebar Drawer

"use client";

import { useState, useEffect } from "react";
import { Menu, X } from "lucide-react";
import { usePathname } from "next/navigation";

export function MobileSidebar({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const pathname = usePathname();

  // Close on navigation
  useEffect(() => {
    setIsOpen(false);
  }, [pathname]);

  return (
    <>
      <button
        onClick={() => setIsOpen(true)}
        className="rounded-lg p-2 lg:hidden"
      >
        <Menu className="h-5 w-5" />
      </button>

      {isOpen && (
        <>
          <div
            className="fixed inset-0 z-40 bg-black/50 lg:hidden"
            onClick={() => setIsOpen(false)}
          />
          <div className="fixed inset-y-0 left-0 z-50 w-64 lg:hidden">
            <div className="absolute right-2 top-2">
              <button
                onClick={() => setIsOpen(false)}
                className="rounded-lg p-1 text-gray-500 hover:bg-gray-100"
              >
                <X className="h-5 w-5" />
              </button>
            </div>
            {children}
          </div>
        </>
      )}
    </>
  );
}

Need a Dashboard UI?

We build dashboards with intuitive navigation, responsive design, and polished user experiences. Contact us to discuss your project.

sidebarnavigationresponsiveReacttutorial

Ready to Start Your Project?

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

Get in Touch

Related Articles