Sidebar
StableThe persistent navigation of an application — collapsible to an icon rail, off-canvas on mobile, and themeable to the last pixel.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/sidebar.jsonUsage
A sidebar is part of your app's layout, not a page. Wrap the layout in SidebarProvider, render your Sidebar next to a SidebarInset that holds the page, and put a SidebarTrigger wherever people should be able to toggle it.
import { cookies } from "next/headers"
import { AppSidebar } from "@/components/app-sidebar"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
export default async function AppLayout({ children }: { children: React.ReactNode }) {
// The provider saves its state in a cookie; read it to render the same state on the server.
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value !== "false"
return (
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar />
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-3">
<SidebarTrigger />
</header>
{children}
</SidebarInset>
</SidebarProvider>
)
}"use client"
import { HouseIcon, InboxIcon, SettingsIcon } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar"
const items = [
{ title: "Home", href: "/", icon: HouseIcon },
{ title: "Inbox", href: "/inbox", icon: InboxIcon },
{ title: "Settings", href: "/settings", icon: SettingsIcon },
]
export function AppSidebar() {
const pathname = usePathname()
return (
<Sidebar collapsible="icon">
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<nav aria-label="Main">
<SidebarMenu>
{items.map((item) => {
const active = pathname === item.href
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
isActive={active}
tooltip={item.title}
render={
<Link href={item.href} aria-current={active ? "page" : undefined} />
}
>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</nav>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
)
}Anatomy
SidebarProvider state, keyboard shortcut, width variables
├─ Sidebar the panel (desktop) or a sheet (mobile)
│ ├─ SidebarHeader workspace switcher, search
│ ├─ SidebarContent scrolls
│ │ └─ SidebarGroup
│ │ ├─ SidebarGroupLabel · SidebarGroupAction
│ │ └─ SidebarGroupContent
│ │ └─ SidebarMenu
│ │ └─ SidebarMenuItem
│ │ ├─ SidebarMenuButton · SidebarMenuAction · SidebarMenuBadge
│ │ └─ SidebarMenuSub → SidebarMenuSubItem → SidebarMenuSubButton
│ ├─ SidebarFooter account, help
│ └─ SidebarRail a clickable edge that toggles the sidebar
└─ SidebarInset the page, with SidebarTrigger in its headerExamples
The previews below are real sidebars inside a box. On desktop the sidebar is position: fixed; the preview gives its wrapper transform: translateZ(0), which makes the wrapper the sidebar's containing block, and renders SidebarInset as a <div> because the docs page already has a <main>. In your app you need neither.
App shell
collapsible="icon" with a SidebarRail and a SidebarTrigger. Click the trigger, click the rail along the sidebar's right edge, or press ⌘ B to collapse it to an icon rail.
Collapsed to icons
Collapsed, labels are clipped and each button shows its tooltip on hover and focus. Give every top-level item an icon and a tooltip if you use this mode.
Variants
sidebar sits flush against the page with a border. floating becomes a raised card inset from the edges. inset recedes into the page background while the content sits on a raised panel — the calmest option for dense apps.
Collapsible sections
Compose Collapsible with the menu to build expandable sections. The collapsible renders the SidebarMenuItem and its trigger renders the SidebarMenuButton, so there are no extra wrappers.
<Collapsible defaultOpen render={<SidebarMenuItem />} className="group/collapsible">
<CollapsibleTrigger render={<SidebarMenuButton />}>
<BookOpenIcon />
<span>Get started</span>
<ChevronRightIcon className="ml-auto transition-transform group-data-open/collapsible:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>…</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>Actions and badges
SidebarMenuBadge shows a count at the end of an item. SidebarMenuAction adds a secondary action — often a menu — that appears on hover with showOnHover. SidebarGroupAction adds one to a group label, like Add project.
Loading
Render SidebarMenuSkeleton while items load. Widths vary per row but are derived from useId, so server and client render the same markup.
Collapsible modes
On screens narrower than 768px every sidebar becomes a Sheet that slides in from its side; SidebarTrigger opens it.
Controlled state
Control open to sync the sidebar with your own state, and read everything from useSidebar() anywhere inside the provider.
const [open, setOpen] = React.useState(true)
<SidebarProvider open={open} onOpenChange={setOpen}>…</SidebarProvider>const { state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar } = useSidebar()Width and side
Widths are CSS variables on the provider — --sidebar-width (16rem) and --sidebar-width-icon (3rem). Pass side="right" for a sidebar on the other edge, such as an inspector panel.
<SidebarProvider
style={{ "--sidebar-width": "18rem", "--sidebar-width-icon": "3.5rem" } as React.CSSProperties}
>
<Sidebar side="right" variant="floating">…</Sidebar>
</SidebarProvider>Theming
The sidebar has its own semantic tokens, so it can differ from the page without touching components.
Guidelines
When to use
- In applications with more than five top-level destinations, or with destinations people move between all day.
- When navigation includes nested sections, projects or saved views that need to stay one click away.
When not to use
- For a handful of destinations — a header with Tabs or a Navigation Menu takes less space.
- On marketing and documentation sites, where content deserves the full width.
- As a place for settings or forms — use a Sheet or a page.
Organize by what people do
Group items by task, label each group, and keep labels to one or two words. Put the workspace or account switcher in the header and help, settings and the profile in the footer, so the middle is only destinations.
Show where people are
Mark exactly one item as current with isActive — and set aria-current="page" on its link so assistive technology hears it too. For nested sections, open the section that contains the current page.
Keep collapse predictable
Pick one collapsible mode for the whole app and remember people's choice (the provider writes a sidebar_state cookie; read it in your layout). Don't collapse the sidebar automatically on navigation.
Accessibility
| Key | Behavior |
|---|---|
Tab | Moves through the sidebar's buttons and links in order. |
EnterSpace | Activates the focused item or toggles a collapsible section. |
⌘BCtrlB | Toggles the sidebar from anywhere in the app — except while typing in a field or rich-text editor. |
Esc | Closes the sidebar sheet on mobile and returns focus to the trigger. |
- Landmarks. Wrap your menu in
<nav aria-label="…">;SidebarInsetrenders the page's<main>. - Current page.
isActiveonly styles an item. Addaria-current="page"to the active link. - Icon rail. Collapsed items keep their text in the DOM, so screen readers still announce the label; tooltips give sighted people the same name.
- Toggle controls.
SidebarTriggeris labelled "Toggle sidebar" — in the language set by LocaleProvider.SidebarRailis a pointer convenience and is removed from the tab order — keyboard users have the trigger and the shortcut. - Mobile. The sheet is a modal dialog: focus moves into it, is trapped while open, and returns to the trigger on close.
API reference
SidebarProvider
Owns the open state, persists it in a cookie and registers the keyboard shortcut. Renders a <div> that lays out the sidebar and the inset side by side.
Sidebar
The navigation panel. On mobile it renders inside a Sheet.
SidebarMenuButton
A menu item's main control. Renders a <button>; pass render={<Link />} for navigation.
SidebarMenuAction
A secondary action at the end of a menu item.
SidebarMenuSubButton
A link inside a sub-menu. Renders an <a> by default.
SidebarInset
The page area next to the sidebar. Renders a <main>; pass render to use another element when the page already has one.
SidebarMenuSkeleton
A placeholder row. showIcon adds an icon-sized block before the text.
Other parts
SidebarHeader, SidebarFooter, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuBadge and SidebarSeparator are styled layout elements that accept their element's props. SidebarGroupLabel and SidebarGroupAction accept render. SidebarInput is a compact Input. SidebarTrigger is a ghost icon Button; SidebarRail is a hover edge that toggles the sidebar.
useSidebar
Returns the sidebar state. Throws outside a SidebarProvider.