Sidebar
shadcn Sidebar component - responsive navigation with collapsible sections, mobile drawer, and persistent state.
Search across all documentation pages
shadcn Sidebar component - responsive navigation with collapsible sections, mobile drawer, and persistent state.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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 {
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:
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={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>// 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">[];
};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 sidebargroup-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>
{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>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 closedmaindefaultOpen 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, 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<NavItem, "icon" | "children">[];
};DropdownMenu inside SidebarFooter > SidebarMenu > SidebarMenuItemSidebarMenuButton as the DropdownMenuTrigger with asChildside="top" and align="start" on DropdownMenuContent so it opens upwardReviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥