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 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.
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
isActivehighlighting for the current page- Icon-collapse mode (
collapsible="icon") — sidebar shrinks to icons only - User menu dropdown in the footer
SidebarTriggertoggle button- Next.js
Linkintegration withasChild
Deep Dive
How It Works
SidebarProvidermanages open/closed state and provides it via contextSidebarrenders 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 viagroup-data-[collapsible=icon]:hiddenuseSidebar()hook exposesstate,open,setOpen,toggleSidebar, andisMobile- State is persisted in a cookie by default (configurable)
SidebarMenuButtonhandles 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
-
SidebarProvidermust 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 inSidebarMenuButton. -
Cookie-based persistence — The default state persistence uses cookies, which requires a server-rendered layout. Fix: In SPAs, pass
defaultOpentoSidebarProvideror manage state yourself. -
asChildon SidebarMenuButton — When using Next.js<Link>, wrap it withasChildso the menu button delegates rendering to the Link. WithoutasChild, you get a button wrapping an anchor (invalid HTML).
Alternatives
| 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 |
FAQs
What is the minimum structure needed for a shadcn Sidebar?
SidebarProviderwrapping bothSidebarandmaincontentSidebarwithSidebarContentcontainingSidebarGroup,SidebarMenu, andSidebarMenuItemSidebarTriggersomewhere 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
mdbreakpoint): renders as a slide-in drawer overlay - Use
isMobilefromuseSidebar()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 setteropenMobile/setOpenMobile: mobile-specific stateisMobile: whether the viewport is below the mobile breakpointtoggleSidebar: 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
defaultOpentoSidebarProvideror 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
asChildwhen 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
DropdownMenuinsideSidebarFooter>SidebarMenu>SidebarMenuItem - Use
SidebarMenuButtonas theDropdownMenuTriggerwithasChild - Set
side="top"andalign="start"onDropdownMenuContentso it opens upward