Dependencies
tailwindcsslucide-react
shadcn/ui components
npx shadcn@latest add buttonnpx shadcn@latest add sheetnpx shadcn@latest add tooltip

A sidebar is the backbone of any admin dashboard. These 4 examples cover the most common patterns — basic nav, collapsible icon mode, dark theme and mobile-responsive with Sheet drawer.

Installation

npx shadcn@latest add button sheet tooltip
npm install lucide-react

Key Patterns

PatternUse case
Basic sidebarSimple dashboards
CollapsibleSpace-constrained layouts
Dark sidebarAdmin panels, developer tools
Sheet on mobileAny responsive dashboard

1. Basic React Sidebar

Clean sidebar with nav links, active state detection and user footer.

tsx
import { useState } from "react"
import { LayoutDashboard, Package, Users, BarChart3, Settings, LogOut } from "lucide-react"

const navItems = [
  { label: "Dashboard", icon: LayoutDashboard, href: "/dashboard", active: true },
  { label: "Templates", icon: Package, href: "/templates", badge: "12" },
  { label: "Customers", icon: Users, href: "/customers" },
  { label: "Analytics", icon: BarChart3, href: "/analytics" },
  { label: "Settings", icon: Settings, href: "/settings" },
]

export default function BasicSidebar() {
  const [active, setActive] = useState("Dashboard")

  return (
    <div className="flex h-screen bg-gray-50">
      {/* Sidebar */}
      <aside className="w-60 bg-white border-r flex flex-col">
        {/* Brand */}
        <div className="px-4 py-4 border-b">
          <div className="flex items-center gap-2">
            <div
              className="w-7 h-7 rounded flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
              style={{ background: "#fd4766" }}
            >
              AP
            </div>
            <span className="font-bold" style={{ color: "#fd4766" }}>AdminPanel</span>
          </div>
        </div>

        {/* Nav */}
        <nav className="flex-1 py-3 px-2">
          <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider px-2 mb-2">Main</p>
          {navItems.map((item) => {
            const Icon = item.icon
            const isActive = active === item.label
            return (
              <a
                key={item.label}
                href={item.href}
                onClick={(e) => { e.preventDefault(); setActive(item.label) }}
                className="flex items-center gap-3 px-3 py-2 rounded-md mb-0.5 text-sm transition-colors"
                style={{
                  color: isActive ? "#fd4766" : "#6b7280",
                  background: isActive ? "rgba(253,71,102,0.08)" : "transparent",
                  fontWeight: isActive ? 600 : 400,
                }}
              >
                <Icon size={17} />
                <span className="flex-1">{item.label}</span>
                {item.badge && (
                  <span
                    className="text-white text-xs px-1.5 py-0.5 rounded-full"
                    style={{ background: "#fd4766", fontSize: "0.65rem" }}
                  >
                    {item.badge}
                  </span>
                )}
              </a>
            )
          })}
        </nav>

        {/* User */}
        <div className="border-t px-3 py-3">
          <div className="flex items-center gap-2">
            <div
              className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
              style={{ background: "#fd4766" }}
            >
              GS
            </div>
            <div className="flex-1 min-w-0">
              <p className="text-sm font-semibold truncate">Gagan Singh</p>
              <p className="text-xs text-gray-400">Founder</p>
            </div>
            <button className="text-gray-400 hover:text-gray-600">
              <LogOut size={15} />
            </button>
          </div>
        </div>
      </aside>

      {/* Main */}
      <main className="flex-1 p-6">
        <h1 className="text-xl font-bold">{active}</h1>
        <p className="text-gray-500 text-sm mt-1">Main content area</p>
      </main>
    </div>
  )
}

2. Collapsible React Sidebar

Sidebar that collapses to icon-only mode with a toggle button and smooth transition.

tsx
import { useState } from "react"
import {
  LayoutDashboard, Package, Users, BarChart3,
  Settings, ChevronLeft, ChevronRight
} from "lucide-react"

const navItems = [
  { label: "Dashboard", icon: LayoutDashboard },
  { label: "Templates", icon: Package },
  { label: "Customers", icon: Users },
  { label: "Analytics", icon: BarChart3 },
  { label: "Settings", icon: Settings },
]

export default function CollapsibleSidebar() {
  const [collapsed, setCollapsed] = useState(false)
  const [active, setActive] = useState("Dashboard")

  return (
    <div className="flex h-screen bg-gray-50">
      <aside
        className="bg-white border-r flex flex-col transition-all duration-300 relative"
        style={{ width: collapsed ? 64 : 240 }}
      >
        {/* Toggle button */}
        <button
          onClick={() => setCollapsed(!collapsed)}
          className="absolute -right-3 top-6 w-6 h-6 bg-white border rounded-full flex items-center justify-center shadow-sm z-10 text-gray-400 hover:text-gray-600"
        >
          {collapsed ? <ChevronRight size={12} /> : <ChevronLeft size={12} />}
        </button>

        {/* Brand */}
        <div className="px-4 py-4 border-b overflow-hidden whitespace-nowrap">
          <div className="flex items-center gap-2">
            <div
              className="w-7 h-7 rounded flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
              style={{ background: "#fd4766" }}
            >
              AP
            </div>
            {!collapsed && (
              <span className="font-bold" style={{ color: "#fd4766" }}>AdminPanel</span>
            )}
          </div>
        </div>

        {/* Nav */}
        <nav className="flex-1 py-3 px-2">
          {navItems.map((item) => {
            const Icon = item.icon
            const isActive = active === item.label
            return (
              <button
                key={item.label}
                onClick={() => setActive(item.label)}
                className="flex items-center gap-3 px-3 py-2.5 rounded-md mb-0.5 w-full transition-colors overflow-hidden whitespace-nowrap"
                title={collapsed ? item.label : undefined}
                style={{
                  color: isActive ? "#fd4766" : "#6b7280",
                  background: isActive ? "rgba(253,71,102,0.08)" : "transparent",
                  fontWeight: isActive ? 600 : 400,
                }}
              >
                <Icon size={17} className="flex-shrink-0" />
                {!collapsed && (
                  <span className="text-sm">{item.label}</span>
                )}
              </button>
            )
          })}
        </nav>

        {/* User */}
        <div className="border-t px-3 py-3 overflow-hidden whitespace-nowrap">
          <div className="flex items-center gap-2">
            <div
              className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
              style={{ background: "#fd4766" }}
            >
              GS
            </div>
            {!collapsed && (
              <div className="min-w-0">
                <p className="text-sm font-semibold truncate">Gagan Singh</p>
                <p className="text-xs text-gray-400">Founder</p>
              </div>
            )}
          </div>
        </div>
      </aside>

      <main className="flex-1 p-6">
        <p className="text-sm text-gray-500">Sidebar is {collapsed ? "collapsed" : "expanded"}</p>
        <h1 className="text-xl font-bold mt-1">{active}</h1>
      </main>
    </div>
  )
}

3. Dark React Sidebar

Dark themed sidebar with grouped sections and notification badges.

tsx
import { useState } from "react"
import {
  LayoutDashboard, Package, Users, BarChart3,
  Settings, Bell, HelpCircle, LogOut
} from "lucide-react"

const sections = [
  {
    label: "Main",
    items: [
      { label: "Dashboard", icon: LayoutDashboard },
      { label: "Templates", icon: Package, badge: "New" },
      { label: "Customers", icon: Users, count: 5 },
      { label: "Analytics", icon: BarChart3 },
    ],
  },
  {
    label: "System",
    items: [
      { label: "Notifications", icon: Bell, count: 3 },
      { label: "Settings", icon: Settings },
      { label: "Help", icon: HelpCircle },
    ],
  },
]

export default function DarkSidebar() {
  const [active, setActive] = useState("Dashboard")

  return (
    <div className="flex h-screen">
      <aside
        className="w-60 flex flex-col"
        style={{ background: "#0d0d0d" }}
      >
        {/* Brand */}
        <div className="px-4 py-4" style={{ borderBottom: "1px solid #1e1e2e" }}>
          <div className="flex items-center gap-2">
            <div
              className="w-7 h-7 rounded flex items-center justify-center text-white text-xs font-bold"
              style={{ background: "#fd4766" }}
            >
              AP
            </div>
            <span className="font-bold text-white">AdminPanel</span>
          </div>
        </div>

        {/* Nav */}
        <nav className="flex-1 py-3 px-2 overflow-y-auto">
          {sections.map((section) => (
            <div key={section.label} className="mb-4">
              <p
                className="text-xs font-semibold uppercase tracking-wider px-2 mb-2"
                style={{ color: "#444" }}
              >
                {section.label}
              </p>
              {section.items.map((item) => {
                const Icon = item.icon
                const isActive = active === item.label
                return (
                  <button
                    key={item.label}
                    onClick={() => setActive(item.label)}
                    className="flex items-center gap-3 px-3 py-2 rounded-md mb-0.5 w-full text-left text-sm transition-colors"
                    style={{
                      color: isActive ? "#fd4766" : "#888",
                      background: isActive ? "rgba(253,71,102,0.12)" : "transparent",
                      fontWeight: isActive ? 600 : 400,
                    }}
                  >
                    <Icon size={16} className="flex-shrink-0" />
                    <span className="flex-1">{item.label}</span>
                    {item.count && (
                      <span
                        className="text-white text-xs px-1.5 py-0.5 rounded-full"
                        style={{ background: "#fd4766", fontSize: "0.6rem" }}
                      >
                        {item.count}
                      </span>
                    )}
                    {item.badge && (
                      <span
                        className="text-xs px-1.5 py-0.5 rounded"
                        style={{ background: "rgba(253,71,102,0.2)", color: "#fd4766", fontSize: "0.62rem" }}
                      >
                        {item.badge}
                      </span>
                    )}
                  </button>
                )
              })}
            </div>
          ))}
        </nav>

        {/* User */}
        <div className="px-3 py-3" style={{ borderTop: "1px solid #1e1e2e" }}>
          <div className="flex items-center gap-2">
            <div
              className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
              style={{ background: "#fd4766" }}
            >
              GS
            </div>
            <div className="flex-1 min-w-0">
              <p className="text-sm font-semibold text-white truncate">Gagan Singh</p>
              <p className="text-xs" style={{ color: "#666" }}>Founder</p>
            </div>
            <button className="hover:text-white transition-colors" style={{ color: "#666" }}>
              <LogOut size={15} />
            </button>
          </div>
        </div>
      </aside>

      <main className="flex-1 p-6" style={{ background: "#f8f9fa" }}>
        <h1 className="text-xl font-bold">{active}</h1>
      </main>
    </div>
  )
}

4. Mobile-Responsive Sidebar with shadcn/ui Sheet

Desktop sidebar + mobile drawer using shadcn/ui Sheet. Hamburger button triggers the mobile menu.

tsx
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import {
  LayoutDashboard, Package, Users,
  BarChart3, Settings, Menu
} from "lucide-react"

const navItems = [
  { label: "Dashboard", icon: LayoutDashboard },
  { label: "Templates", icon: Package },
  { label: "Customers", icon: Users },
  { label: "Analytics", icon: BarChart3 },
  { label: "Settings", icon: Settings },
]

function SidebarContent({ active, setActive }: { active: string; setActive: (v: string) => void }) {
  return (
    <div className="flex flex-col h-full">
      <div className="px-4 py-4 border-b">
        <span className="font-bold text-lg" style={{ color: "#fd4766" }}>AdminPanel</span>
      </div>
      <nav className="flex-1 py-3 px-2">
        {navItems.map((item) => {
          const Icon = item.icon
          const isActive = active === item.label
          return (
            <button
              key={item.label}
              onClick={() => setActive(item.label)}
              className="flex items-center gap-3 px-3 py-2.5 rounded-md mb-0.5 w-full text-left text-sm transition-colors"
              style={{
                color: isActive ? "#fd4766" : "#6b7280",
                background: isActive ? "rgba(253,71,102,0.08)" : "transparent",
                fontWeight: isActive ? 600 : 400,
              }}
            >
              <Icon size={17} />
              {item.label}
            </button>
          )
        })}
      </nav>
      <div className="border-t px-4 py-3">
        <p className="text-xs text-gray-500">
          Get templates at{" "}
          <a href="https://lettstartdesign.com" className="font-semibold" style={{ color: "#fd4766" }}>
            LettStartDesign
          </a>
        </p>
      </div>
    </div>
  )
}

export default function MobileResponsiveSidebar() {
  const [active, setActive] = useState("Dashboard")

  return (
    <div className="flex h-screen bg-gray-50">
      {/* Desktop sidebar */}
      <aside className="hidden lg:flex w-60 bg-white border-r">
        <SidebarContent active={active} setActive={setActive} />
      </aside>

      {/* Main */}
      <div className="flex-1 flex flex-col min-w-0">
        {/* Mobile header */}
        <header className="lg:hidden bg-white border-b px-4 py-3 flex items-center gap-3">
          <Sheet>
            <SheetTrigger asChild>
              <Button variant="outline" size="icon" className="h-8 w-8">
                <Menu size={16} />
              </Button>
            </SheetTrigger>
            <SheetContent side="left" className="w-60 p-0">
              <SidebarContent active={active} setActive={setActive} />
            </SheetContent>
          </Sheet>
          <span className="font-bold" style={{ color: "#fd4766" }}>AdminPanel</span>
        </header>

        <main className="flex-1 p-6">
          <h1 className="text-xl font-bold">{active}</h1>
          <p className="text-gray-500 text-sm mt-1">
            Resize to mobile to see the hamburger menu
          </p>
        </main>
      </div>
    </div>
  )
}

Frequently Asked Questions

Use position:fixed or position:sticky with height:100vh. For fixed: set the main content margin-left equal to sidebar width. For sticky: wrap in a flex container where the sidebar doesn't scroll with the page content. The flex h-screen approach in these examples is the cleanest.
Track a boolean state with useState. Conditionally render text labels based on the collapsed state. Animate the width with CSS transition: transition-all duration-300 on the aside element. Set width to 64px when collapsed (icon only) and 240px when expanded.
Save to localStorage: localStorage.setItem('sidebarCollapsed', JSON.stringify(collapsed)). Load on init: const [collapsed, setCollapsed] = useState(() => JSON.parse(localStorage.getItem('sidebarCollapsed') || 'false')). This persists across refreshes and navigation.
Hide the desktop sidebar on mobile with hidden lg:flex. Create a mobile header with a hamburger button. Use shadcn/ui Sheet as a drawer on mobile. Share the same nav content component between desktop sidebar and mobile Sheet to avoid duplication.

Need a Full React + Next.js Dashboard Template?

Get a complete React + Next.js dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off.

Related Components