Sidebar
The persistent navigation of an application. It collapses to an icon rail, moves off-canvas on mobile and is themeable to the last pixel.
import {
CalendarIcon,
ChevronsUpDownIcon,
FolderKanbanIcon,
HouseIcon,
InboxIcon,
SearchIcon,
} from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@/components/ui/sidebar"
const navigation = [
{ title: "Home", icon: HouseIcon, active: true },
{ title: "Inbox", icon: InboxIcon, badge: 12 },
{ title: "Projects", icon: FolderKanbanIcon },
{ title: "Calendar", icon: CalendarIcon },
{ title: "Search", icon: SearchIcon },
]
const favorites = [
{ title: "Design system", color: "bg-brand-9" },
{ title: "Q4 launch", color: "bg-amber-9" },
{ title: "Customer research", color: "bg-teal-9" },
]
export default function SidebarDemo() {
return (
<SidebarProvider className="min-h-0 w-fit">
<Sidebar
collapsible="none"
className="h-104 rounded-xl border shadow-raised"
>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background">
A
</span>
<span className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate font-semibold text-foreground">
Acme Inc
</span>
<span className="truncate text-xs text-muted-foreground">
Pro plan
</span>
</span>
<ChevronsUpDownIcon className="ml-auto" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{navigation.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton isActive={item.active}>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
{item.badge !== undefined && (
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Favorites</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{favorites.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton>
<span
className={`ml-1 size-2 shrink-0 rounded-full ${item.color}`}
/>
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg">
<Avatar size="sm">
<AvatarFallback>AK</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate font-medium text-foreground">
Ana Kowalska
</span>
<span className="truncate text-xs text-muted-foreground">
ana@acme.com
</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
</SidebarProvider>
)
}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.
import {
BookOpenIcon,
ChartBarIcon,
FolderKanbanIcon,
HouseIcon,
InboxIcon,
LifeBuoyIcon,
SettingsIcon,
} from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar"
const platform = [
{ title: "Home", icon: HouseIcon },
{ title: "Inbox", icon: InboxIcon },
{ title: "Projects", icon: FolderKanbanIcon, active: true },
{ title: "Analytics", icon: ChartBarIcon },
]
const support = [
{ title: "Documentation", icon: BookOpenIcon },
{ title: "Help center", icon: LifeBuoyIcon },
{ title: "Settings", icon: SettingsIcon },
]
export default function SidebarAppShell() {
return (
// `transform` makes this box the containing block for the sidebar's fixed
// positioning, so the whole app shell fits inside the preview.
<SidebarProvider className="h-120 min-h-0 [transform:translateZ(0)] overflow-hidden rounded-xl border">
<Sidebar collapsible="icon" className="h-auto">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" tooltip="Acme Inc">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background">
A
</span>
<span className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate font-semibold text-foreground">
Acme Inc
</span>
<span className="truncate text-xs text-muted-foreground">
Pro plan
</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{platform.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
isActive={item.active}
tooltip={item.title}
>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup className="mt-auto">
<SidebarGroupLabel>Support</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{support.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton size="sm" tooltip={item.title}>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" tooltip="Ana Kowalska">
<Avatar size="sm">
<AvatarFallback>AK</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate font-medium text-foreground">
Ana Kowalska
</span>
<span className="truncate text-xs text-muted-foreground">
ana@acme.com
</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
{/* The docs page already has a <main>, so the preview renders a <div>. */}
<SidebarInset render={<div />}>
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
<SidebarTrigger />
<Separator
orientation="vertical"
className="mr-1 h-4 data-vertical:self-center"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Design system</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<div className="grid flex-1 auto-rows-min gap-3 p-4 sm:grid-cols-3">
<div className="aspect-video rounded-lg bg-muted" />
<div className="aspect-video rounded-lg bg-muted" />
<div className="aspect-video rounded-lg bg-muted" />
<div className="h-40 rounded-lg bg-muted sm:col-span-3" />
</div>
</SidebarInset>
</SidebarProvider>
)
}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.
import {
ChartBarIcon,
FolderKanbanIcon,
HouseIcon,
InboxIcon,
SettingsIcon,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
const items = [
{ title: "Home", icon: HouseIcon, active: true },
{ title: "Inbox", icon: InboxIcon },
{ title: "Projects", icon: FolderKanbanIcon },
{ title: "Analytics", icon: ChartBarIcon },
]
export default function SidebarIconCollapsed() {
return (
<SidebarProvider
defaultOpen={false}
className="h-80 min-h-0 [transform:translateZ(0)] overflow-hidden rounded-xl border"
>
<Sidebar collapsible="icon" className="h-auto">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" tooltip="Acme Inc">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background">
A
</span>
<span className="truncate font-semibold text-foreground">
Acme Inc
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
isActive={item.active}
tooltip={item.title}
>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton tooltip="Settings">
<SettingsIcon />
<span>Settings</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
<SidebarInset render={<div />}>
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
<SidebarTrigger />
<span className="text-sm font-medium text-foreground">Home</span>
</header>
<div className="flex flex-1 flex-col gap-3 p-4">
<div className="h-24 rounded-lg bg-muted" />
<div className="flex-1 rounded-lg bg-muted" />
</div>
</SidebarInset>
</SidebarProvider>
)
}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.
"use client"
import * as React from "react"
import { FolderKanbanIcon, HouseIcon, InboxIcon } from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
type Variant = "sidebar" | "floating" | "inset"
const items = [
{ title: "Home", icon: HouseIcon, active: true },
{ title: "Inbox", icon: InboxIcon },
{ title: "Projects", icon: FolderKanbanIcon },
]
export default function SidebarVariants() {
const [variant, setVariant] = React.useState<Variant>("inset")
return (
<div className="flex w-full flex-col items-center gap-4">
<ToggleGroup
variant="outline"
size="sm"
spacing={0}
aria-label="Sidebar variant"
value={[variant]}
onValueChange={(value) => value[0] && setVariant(value[0] as Variant)}
>
<ToggleGroupItem value="sidebar">Sidebar</ToggleGroupItem>
<ToggleGroupItem value="floating">Floating</ToggleGroupItem>
<ToggleGroupItem value="inset">Inset</ToggleGroupItem>
</ToggleGroup>
<SidebarProvider className="h-80 min-h-0 w-full [transform:translateZ(0)] overflow-hidden rounded-xl border">
<Sidebar variant={variant} collapsible="icon" className="h-auto">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Workspace</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
isActive={item.active}
tooltip={item.title}
>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset render={<div />}>
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
<SidebarTrigger />
<span className="text-sm font-medium text-foreground capitalize">
{variant}
</span>
</header>
<div className="flex flex-1 flex-col gap-3 p-4">
<div className="h-20 rounded-lg bg-muted" />
<div className="flex-1 rounded-lg bg-muted" />
</div>
</SidebarInset>
</SidebarProvider>
</div>
)
}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.
import {
BookOpenIcon,
ChevronRightIcon,
LayersIcon,
PaletteIcon,
} from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
} from "@/components/ui/sidebar"
const sections = [
{
title: "Get started",
icon: BookOpenIcon,
open: true,
items: ["Introduction", "Installation", "Principles"],
active: "Installation",
},
{
title: "Foundations",
icon: PaletteIcon,
items: ["Color", "Typography", "Motion"],
},
{
title: "Components",
icon: LayersIcon,
items: ["Button", "Dialog", "Sidebar"],
},
]
export default function SidebarSubmenu() {
return (
<SidebarProvider className="min-h-0 w-fit">
<Sidebar
collapsible="none"
className="h-96 rounded-xl border shadow-raised"
>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Documentation</SidebarGroupLabel>
<SidebarMenu>
{sections.map((section) => (
<Collapsible
key={section.title}
defaultOpen={section.open}
render={<SidebarMenuItem />}
className="group/collapsible"
>
<CollapsibleTrigger render={<SidebarMenuButton />}>
<section.icon />
<span>{section.title}</span>
<ChevronRightIcon className="ml-auto transition-transform duration-base ease-standard group-data-open/collapsible:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{section.items.map((item) => (
<SidebarMenuSubItem key={item}>
<SidebarMenuSubButton
render={<button type="button" />}
isActive={item === section.active}
className="w-full"
>
<span>{item}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarProvider>
)
}<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.
import {
ArchiveIcon,
InboxIcon,
MoreHorizontalIcon,
PencilIcon,
PlusIcon,
SendIcon,
Trash2Icon,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@/components/ui/sidebar"
const mail = [
{ title: "Inbox", icon: InboxIcon, count: 24 },
{ title: "Sent", icon: SendIcon },
{ title: "Archive", icon: ArchiveIcon, count: 3 },
]
const projects = [
{ title: "Design system", color: "bg-brand-9" },
{ title: "Marketing site", color: "bg-pink-9" },
{ title: "Mobile app", color: "bg-green-9" },
]
export default function SidebarActions() {
return (
<SidebarProvider className="min-h-0 w-fit">
<Sidebar
collapsible="none"
className="h-88 rounded-xl border shadow-raised"
>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Mail</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{mail.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton isActive={item.title === "Inbox"}>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
{item.count !== undefined && (
<SidebarMenuBadge>{item.count}</SidebarMenuBadge>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarGroupAction title="Add project">
<PlusIcon />
<span className="sr-only">Add project</span>
</SidebarGroupAction>
<SidebarGroupContent>
<SidebarMenu>
{projects.map((project) => (
<SidebarMenuItem key={project.title}>
<SidebarMenuButton>
<span
className={`ml-1 size-2 shrink-0 rounded-full ${project.color}`}
/>
<span>{project.title}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger
render={<SidebarMenuAction showOnHover />}
>
<MoreHorizontalIcon />
<span className="sr-only">
More actions for {project.title}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
align="start"
className="w-44"
>
<DropdownMenuGroup>
<DropdownMenuItem>
<PencilIcon />
Rename
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Delete
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarProvider>
)
}Loading
Render SidebarMenuSkeleton while items load. Widths vary per row but are derived from useId, so server and client render the same markup.
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarProvider,
} from "@/components/ui/sidebar"
export default function SidebarSkeleton() {
return (
<SidebarProvider className="min-h-0 w-fit">
<Sidebar
collapsible="none"
className="h-72 rounded-xl border shadow-raised"
>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu aria-busy="true" aria-label="Loading projects">
{Array.from({ length: 6 }, (_, index) => (
<SidebarMenuItem key={index}>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarProvider>
)
}Collapsible modes
On screens narrower than 768px an offcanvas or icon sidebar becomes a Sheet that slides in from its side; SidebarTrigger opens it. A none sidebar stays in the layout at every width, so hide or restyle it yourself on small screens.
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. It styles the item and sets aria-current="page" on it, 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. Besides styling an item,
isActivesetsaria-current="page"on its button or 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.