Skip to content

Sidebar

Stable

The persistent navigation of an application — collapsible to an icon rail, off-canvas on mobile, and themeable to the last pixel.

  • 12
Favorites

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/sidebar.json

Usage

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.

app/(app)/layout.tsx
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>
  )
}
components/app-sidebar.tsx
"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 header

Examples

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.

Home

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.

inset

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.

Documentation
<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.

Mail
  • 24
  • 3
Projects

Loading

Render SidebarMenuSkeleton while items load. Widths vary per row but are derived from useId, so server and client render the same markup.

Projects

Collapsible modes

collapsibleWhen collapsed on desktopChoose it when
offcanvas (default)Slides fully out of view.Content needs all the width; navigation is occasional.
iconShrinks to a 3rem rail of icons with tooltips.People switch sections constantly.
noneNever collapses; renders as a plain panel.Settings pages, embedded navigation, previews.

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.

TokenDefault (light / dark)Role
--sidebargray-1 / gray-1Surface
--sidebar-foregroundgray-12Text
--sidebar-accentgray-4Hovered and active items
--sidebar-accent-foregroundgray-12Text on accent
--sidebar-bordergray-6 / gray-5Borders, sub-menu rule
--sidebar-ringbrand-9 / brand-10Focus ring

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.

WorkspaceHomeInbox
ProjectsDesign systemQ4 launch
Do.Labelled groups of short, parallel destinations.
HomeCreate a new projectDesign systemNotification settingsInbox
Don’t.One long unlabelled list, mixing destinations with actions and settings.

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

KeyBehavior
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="…">; SidebarInset renders the page's <main>.
  • Current page. isActive only styles an item. Add aria-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. SidebarTrigger is labelled "Toggle sidebar" — in the language set by LocaleProvider. SidebarRail is 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.

PropTypeDefault
defaultOpen

Whether the desktop sidebar starts expanded (uncontrolled).

booleantrue
open

Whether the desktop sidebar is expanded (controlled). Pair with onOpenChange.

booleanNo default
onOpenChange

Called when the desktop sidebar expands or collapses.

(open: boolean) => voidNo default
style

Override --sidebar-width and --sidebar-width-icon.

CSSPropertiesNo default

The navigation panel. On mobile it renders inside a Sheet.

PropTypeDefault
side

The edge the sidebar attaches to.

"left" | "right""left"
variant

Visual treatment on desktop.

"sidebar" | "floating" | "inset""sidebar"
collapsible

How the sidebar collapses on desktop.

"offcanvas" | "icon" | "none""offcanvas"

SidebarMenuButton

A menu item's main control. Renders a <button>; pass render={<Link />} for navigation.

PropTypeDefault
isActive

Styles the item as current (data-active). Also set aria-current on links.

booleanfalse
variant

outline adds a hairline border.

"default" | "outline""default"
size

Row height: 32px, 28px or 48px.

"default" | "sm" | "lg""default"
tooltip

Label shown on hover when the sidebar is collapsed to icons.

string | TooltipContent propsNo default
render

Renders a different element, such as a Next.js Link.

ReactElement | (props, state) => ReactElementNo default

SidebarMenuAction

A secondary action at the end of a menu item.

PropTypeDefault
showOnHover

Hides the action until the item is hovered or focused (desktop only).

booleanfalse
render

Renders a different element, e.g. a DropdownMenuTrigger.

ReactElement | (props, state) => ReactElementNo default

SidebarMenuSubButton

A link inside a sub-menu. Renders an <a> by default.

PropTypeDefault
isActive

Styles the item as current.

booleanfalse
size

Text size of the item.

"sm" | "md""md"
render

Renders a different element, such as a Link or a button.

ReactElement | (props, state) => ReactElementNo 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.

PropTypeDefault
state

Desktop state, mirrored as data-state on the sidebar.

"expanded" | "collapsed"No default
open / setOpen

Desktop open state.

boolean / (open: boolean) => voidNo default
openMobile / setOpenMobile

Mobile sheet state.

boolean / (open: boolean) => voidNo default
isMobile

True below the md breakpoint (768px).

booleanNo default
toggleSidebar

Toggles the right state for the current viewport.

() => voidNo default