React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

shadcnsidebarnavigationlayoutcollapsible

Sidebar

shadcn Sidebar component — responsive navigation with collapsible sections, mobile drawer, and persistent state.

Recipe

Quick-reference recipe card — copy-paste ready.

npx shadcn@latest add sidebar
import {
  Sidebar, SidebarContent, SidebarFooter, SidebarGroup,
  SidebarGroupContent, SidebarGroupLabel, SidebarHeader,
  SidebarMenu, SidebarMenuButton, SidebarMenuItem,
  SidebarProvider, SidebarTrigger,
} from "@/components/ui/sidebar";
 
export function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar>
        <SidebarHeader>
          <h2 className="px-4 text-lg font-bold">My App</h2>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Main</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton asChild>
                    <a href="/">Dashboard</a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton asChild>
                    <a href="/settings">Settings</a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <p className="px-4 text-xs text-muted-foreground">v1.0.0</p>
        </SidebarFooter>
      </Sidebar>
      <main className="flex-1">
        <SidebarTrigger className="m-4" />
        {children}
      </main>
    </SidebarProvider>
  );
}

When to reach for this: When your app needs persistent navigation — dashboards, admin panels, documentation sites, or any multi-page application.

Working Example

"use client";
 
import { usePathname } from "next/navigation";
import Link from "next/link";
import {
  Sidebar, SidebarContent, SidebarFooter, SidebarGroup,
  SidebarGroupContent, SidebarGroupLabel, SidebarHeader,
  SidebarMenu, SidebarMenuButton, SidebarMenuItem,
  SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem,
  SidebarProvider, SidebarTrigger, useSidebar,
} from "@/components/ui/sidebar";
import {
  Collapsible, CollapsibleContent, CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
  DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  LayoutDashboard, Users, Settings, FileText,
  ChevronDown, LogOut, ChevronsUpDown,
} from "lucide-react";
 
type NavItem = {
  title: string;
  href: string;
  icon: React.ReactNode;
  children?: { title: string; href: string }[];
};
 
const navItems: NavItem[] = [
  { title: "Dashboard", href: "/dashboard", icon: <LayoutDashboard className="h-4 w-4" /> },
  {
    title: "Users",
    href: "/users",
    icon: <Users className="h-4 w-4" />,
    children: [
      { title: "All Users", href: "/users" },
      { title: "Roles", href: "/users/roles" },
      { title: "Invitations", href: "/users/invitations" },
    ],
  },
  {
    title: "Content",
    href: "/content",
    icon: <FileText className="h-4 w-4" />,
    children: [
      { title: "Posts", href: "/content/posts" },
      { title: "Pages", href: "/content/pages" },
      { title: "Media", href: "/content/media" },
    ],
  },
  { title: "Settings", href: "/settings", icon: <Settings className="h-4 w-4" /> },
];
 
function AppSidebar() {
  const pathname = usePathname();
 
  return (
    <Sidebar collapsible="icon">
      <SidebarHeader className="border-b px-4 py-3">
        <div className="flex items-center gap-2">
          <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
            A
          </div>
          <span className="font-semibold group-data-[collapsible=icon]:hidden">
            Acme Inc
          </span>
        </div>
      </SidebarHeader>
 
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Navigation</SidebarGroupLabel>
          <SidebarGroupContent>
            <SidebarMenu>
              {navItems.map((item) =>
                item.children ? (
                  <Collapsible key={item.title} defaultOpen={pathname.startsWith(item.href)}>
                    <SidebarMenuItem>
                      <CollapsibleTrigger asChild>
                        <SidebarMenuButton>
                          {item.icon}
                          <span>{item.title}</span>
                          <ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
                        </SidebarMenuButton>
                      </CollapsibleTrigger>
                      <CollapsibleContent>
                        <SidebarMenuSub>
                          {item.children.map((child) => (
                            <SidebarMenuSubItem key={child.href}>
                              <SidebarMenuSubButton
                                asChild
                                isActive={pathname === child.href}
                              >
                                <Link href={child.href}>{child.title}</Link>
                              </SidebarMenuSubButton>
                            </SidebarMenuSubItem>
                          ))}
                        </SidebarMenuSub>
                      </CollapsibleContent>
                    </SidebarMenuItem>
                  </Collapsible>
                ) : (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton asChild isActive={pathname === item.href}>
                      <Link href={item.href}>
                        {item.icon}
                        <span>{item.title}</span>
                      </Link>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                )
              )}
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
 
      <SidebarFooter className="border-t">
        <SidebarMenu>
          <SidebarMenuItem>
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <SidebarMenuButton className="w-full">
                  <div className="flex h-6 w-6 items-center justify-center rounded-full bg-muted text-xs">
                    JD
                  </div>
                  <span className="group-data-[collapsible=icon]:hidden">John Doe</span>
                  <ChevronsUpDown className="ml-auto h-4 w-4 group-data-[collapsible=icon]:hidden" />
                </SidebarMenuButton>
              </DropdownMenuTrigger>
              <DropdownMenuContent side="top" align="start" className="w-48">
                <DropdownMenuItem>Profile</DropdownMenuItem>
                <DropdownMenuItem>Billing</DropdownMenuItem>
                <DropdownMenuItem>
                  <LogOut className="mr-2 h-4 w-4" /> Log out
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarFooter>
    </Sidebar>
  );
}
 
export function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <main className="flex-1 overflow-auto">
        <header className="flex items-center gap-4 border-b px-6 py-3">
          <SidebarTrigger />
          <h1 className="text-lg font-semibold">Dashboard</h1>
        </header>
        <div className="p-6">{children}</div>
      </main>
    </SidebarProvider>
  );
}

What this demonstrates:

  • Full dashboard sidebar with header, content groups, and footer
  • Collapsible sub-menus that auto-open based on current path
  • isActive highlighting for the current page
  • Icon-collapse mode (collapsible="icon") — sidebar shrinks to icons only
  • User menu dropdown in the footer
  • SidebarTrigger toggle button
  • Next.js Link integration with asChild

Deep Dive

How It Works

  • SidebarProvider manages open/closed state and provides it via context
  • Sidebar renders a <aside> with fixed width and responsive behavior
  • On mobile (below md), the sidebar becomes a slide-in drawer
  • collapsible="icon" shrinks the sidebar to show only icons, hiding text via group-data-[collapsible=icon]:hidden
  • useSidebar() hook exposes state, open, setOpen, toggleSidebar, and isMobile
  • State is persisted in a cookie by default (configurable)
  • SidebarMenuButton handles active state, tooltip in collapsed mode, and keyboard navigation

Variations

Controlled sidebar state:

"use client";
 
import { useSidebar } from "@/components/ui/sidebar";
 
function SidebarToggle() {
  const { open, setOpen, toggleSidebar, isMobile } = useSidebar();
 
  return (
    <button onClick={toggleSidebar}>
      {open ? "Collapse" : "Expand"}
    </button>
  );
}

Sidebar with badges:

<SidebarMenuButton>
  <Users className="h-4 w-4" />
  <span>Users</span>
  <span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
    12
  </span>
</SidebarMenuButton>

Sidebar with search:

<SidebarHeader>
  <div className="px-2 py-2">
    <Input placeholder="Search..." className="h-8" />
  </div>
</SidebarHeader>

Right-side sidebar:

<SidebarProvider>
  <main className="flex-1">{children}</main>
  <Sidebar side="right" collapsible="none">
    {/* Properties panel, chat, etc. */}
  </Sidebar>
</SidebarProvider>

TypeScript Notes

// useSidebar hook return type
const sidebar: {
  state: "expanded" | "collapsed";
  open: boolean;
  setOpen: (open: boolean) => void;
  openMobile: boolean;
  setOpenMobile: (open: boolean) => void;
  isMobile: boolean;
  toggleSidebar: () => void;
} = useSidebar();
 
// Nav item type
type NavItem = {
  title: string;
  href: string;
  icon: React.ComponentType<{ className?: string }>;
  badge?: number;
  children?: Omit<NavItem, "icon" | "children">[];
};

Gotchas

  • SidebarProvider must wrap both Sidebar and main content — The provider manages layout. Placing it wrong breaks the flex container. Fix: Wrap at the layout level.

  • Mobile vs desktop behavior — On mobile, the sidebar renders as a drawer overlay; on desktop, it is inline. If you conditionally render based on isMobile, ensure you handle hydration.

  • collapsible="icon" needs icons — If your menu items do not have icons, collapsed mode shows empty space. Fix: Always include an icon in SidebarMenuButton.

  • Cookie-based persistence — The default state persistence uses cookies, which requires a server-rendered layout. Fix: In SPAs, pass defaultOpen to SidebarProvider or manage state yourself.

  • asChild on SidebarMenuButton — When using Next.js <Link>, wrap it with asChild so the menu button delegates rendering to the Link. Without asChild, you get a button wrapping an anchor (invalid HTML).

Alternatives

AlternativeUse WhenDon't Use When
Custom sidebarYou need behavior not covered by the shadcn componentThe shadcn Sidebar covers your needs
Tab navigationMobile-first app with bottom tabsDesktop dashboard with many nav items
Top navbar onlyYour app has fewer than 6 top-level pagesYou need nested, multi-level navigation
Drawer (Sheet)You want a temporary slide-in panel, not persistent navYou need always-visible navigation

FAQs

What is the minimum structure needed for a shadcn Sidebar?
  • SidebarProvider wrapping both Sidebar and main content
  • Sidebar with SidebarContent containing SidebarGroup, SidebarMenu, and SidebarMenuItem
  • SidebarTrigger somewhere in the main area to toggle the sidebar
What does collapsible="icon" do?
  • Shrinks the sidebar to show only icons, hiding text labels
  • Text elements are hidden via the CSS selector group-data-[collapsible=icon]:hidden
  • Menu items must include icons or collapsed mode shows empty space
How does the sidebar behave differently on mobile vs desktop?
  • Desktop: renders inline as an <aside> with fixed width
  • Mobile (below md breakpoint): renders as a slide-in drawer overlay
  • Use isMobile from useSidebar() to detect the current mode
How do you build collapsible sub-menus that auto-open based on the current route?
<Collapsible defaultOpen={pathname.startsWith(item.href)}>
  <SidebarMenuItem>
    <CollapsibleTrigger asChild>
      <SidebarMenuButton>{item.icon} {item.title}</SidebarMenuButton>
    </CollapsibleTrigger>
    <CollapsibleContent>
      <SidebarMenuSub>
        {item.children.map((child) => (
          <SidebarMenuSubItem key={child.href}>
            <SidebarMenuSubButton asChild isActive={pathname === child.href}>
              <Link href={child.href}>{child.title}</Link>
            </SidebarMenuSubButton>
          </SidebarMenuSubItem>
        ))}
      </SidebarMenuSub>
    </CollapsibleContent>
  </SidebarMenuItem>
</Collapsible>
What does the useSidebar() hook return?
  • state: "expanded" or "collapsed"
  • open / setOpen: boolean open state and setter
  • openMobile / setOpenMobile: mobile-specific state
  • isMobile: whether the viewport is below the mobile breakpoint
  • toggleSidebar: toggles between open and closed
Gotcha: Why must SidebarProvider wrap both the Sidebar and main content?
  • The provider manages layout via a flex container
  • Placing it only around the Sidebar breaks the flex relationship with main
  • Fix: wrap at the layout level so both Sidebar and main are siblings
How is sidebar state persisted across page navigations?
  • By default, the open/closed state is saved in a cookie
  • This requires a server-rendered layout to read the cookie on initial load
  • In SPAs without SSR, pass defaultOpen to SidebarProvider or manage state yourself
How do you render a sidebar on the right side of the page?
<SidebarProvider>
  <main className="flex-1">{children}</main>
  <Sidebar side="right" collapsible="none">
    {/* Properties panel, chat, etc. */}
  </Sidebar>
</SidebarProvider>
Gotcha: Why does using Next.js Link without asChild on SidebarMenuButton produce invalid HTML?
  • Without asChild, SidebarMenuButton renders a <button> and your <Link> renders an <a> inside it
  • A <button> wrapping an <a> is invalid HTML
  • Fix: always use asChild when the child is a <Link> or <a> element
How do you type a navigation item for a sidebar with optional children?
type NavItem = {
  title: string;
  href: string;
  icon: React.ComponentType<{ className?: string }>;
  badge?: number;
  children?: Omit<NavItem, "icon" | "children">[];
};
How do you add a user menu dropdown in the sidebar footer?
  • Place a DropdownMenu inside SidebarFooter > SidebarMenu > SidebarMenuItem
  • Use SidebarMenuButton as the DropdownMenuTrigger with asChild
  • Set side="top" and align="start" on DropdownMenuContent so it opens upward
  • Setup — installing shadcn components
  • Button — sidebar trigger buttons
  • Command — command palette as navigation complement
  • Dialog — mobile sidebar uses dialog patterns