Search across all documentation pages
shadcn Sidebar component — responsive navigation with collapsible sections, mobile drawer, and persistent state.
Quick-reference recipe card — copy-paste ready.
npx shadcn@latest add sidebarimport {
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.
"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
What this demonstrates:
isActive highlighting for the current pagecollapsible="icon") — sidebar shrinks to icons onlySidebarTrigger toggle buttonLink integration with asChildSidebarProvider manages open/closed state and provides it via contextSidebar renders a <aside> with fixed width and responsive behaviormd), the sidebar becomes a slide-in drawercollapsible="icon" shrinks the sidebar to show only icons, hiding text via group-data-[collapsible=icon]:hiddenuseSidebar() hook exposes state, open, setOpen, toggleSidebar, and isMobileSidebarMenuButton handles active state, tooltip in collapsed mode, and keyboard navigationControlled sidebar state:
"use client";
import { useSidebar } from "@/components/ui/sidebar";
function SidebarToggle() {
const { open, setOpen, toggleSidebar, isMobile } = useSidebar();
return (
<button onClick
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>// useSidebar hook return type
const sidebar: {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile
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).
| Alternative | Use When | Don't Use When |
|---|---|---|
| Custom sidebar | You need behavior not covered by the shadcn component | The shadcn Sidebar covers your needs |
| Tab navigation | Mobile-first app with bottom tabs | Desktop dashboard with many nav items |
| Top navbar only | Your app has fewer than 6 top-level pages | You need nested, multi-level navigation |
| Drawer (Sheet) | You want a temporary slide-in panel, not persistent nav | You need always-visible navigation |
SidebarProvider wrapping both Sidebar and main contentSidebar with SidebarContent containing SidebarGroup, SidebarMenu, and SidebarMenuItemSidebarTrigger somewhere in the main area to toggle the sidebarcollapsible="icon" do?group-data-[collapsible=icon]:hidden<aside> with fixed widthmd breakpoint): renders as a slide-in drawer overlayisMobile from useSidebar() to detect the current mode<Collapsible defaultOpen={pathname.startsWith(item.href)}>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>{item.icon} {item.title}</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub
useSidebar() hook return?state: "expanded" or "collapsed"open / setOpen: boolean open state and setteropenMobile / setOpenMobile: mobile-specific stateisMobile: whether the viewport is below the mobile breakpointtoggleSidebar: toggles between open and closedSidebarProvider wrap both the Sidebar and main content?maindefaultOpen to SidebarProvider or manage state yourself<SidebarProvider>
<main className="flex-1">{children}</main>
<Sidebar side="right" collapsible="none">
{/* Properties panel, chat, etc. */}
</Sidebar>
</SidebarProvider>asChild on SidebarMenuButton produce invalid HTML?asChild, SidebarMenuButton renders a <button> and your <Link> renders an <a> inside it<button> wrapping an <a> is invalid HTMLasChild when the child is a <Link> or <a> elementtype NavItem = {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
badge?: number;
children?: Omit
DropdownMenu inside SidebarFooter > SidebarMenu > SidebarMenuItemSidebarMenuButton as the DropdownMenuTrigger with asChildside="top" and align="start" on DropdownMenuContent so it opens upward