Skip to content
Navigation

Sidebar

A composable, stateful sidebar system with collapse modes, a built-in mobile drawer, keyboard toggle (⌘/Ctrl+B), and cookie-persisted state. Use it when you need a richer sidebar than the App Shell provides — collapse-to-icon, floating/inset variants, or nested menus.

Anatomy

SidebarHeader

SidebarGroupLabel

SidebarMenuButton (active)

SidebarMenuButton

SidebarFooter

SidebarTrigger

SidebarInset (page content)

Wrap everything in SidebarProvider. It manages open state (persisted to a sidebar_state cookie), a ⌘/Ctrl+B toggle, and swaps the sidebar for a mobile drawer on small screens. Read or toggle state anywhere via useSidebar().

Import

import {
  SidebarProvider,
  Sidebar,
  SidebarHeader,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuButton,
  SidebarTrigger,
  SidebarInset,
  useSidebar,
} from "@mikenotthepope/substrateui"

Composition

SidebarProvider
├── Sidebar
│   ├── SidebarHeader
│   ├── SidebarContent
│   │   └── SidebarGroup
│   │       ├── SidebarGroupLabel
│   │       └── SidebarMenu
│   │           └── SidebarMenuItem
│   │               └── SidebarMenuButton
│   └── SidebarFooter
└── SidebarInset
    └── SidebarTrigger

When to use this vs. App Shell

AppShell is a lightweight, opinionated side-nav layout — reach for it first. Use Sidebar when you need collapse-to-icon, floating/inset variants, nested submenus, badges, or programmatic control over the open state.

Labels

The trigger is an icon button, and the mobile sheet needs a title and description whether or not the design shows them. Override one instance with the labels prop, or every instance at once through LabelsProvider's sidebar key — the provider is how you translate the set once instead of at each call site.

PropTypeDefaultDescription
toggleSidebarstring"Toggle Sidebar"Accessible name for SidebarTrigger, whose visible content is an icon.
mobileTitlestring"Sidebar"Title of the mobile sheet. Required by the dialog underneath, and visually hidden.
mobileDescriptionstring"Displays the mobile sidebar."Description of the mobile sheet, also visually hidden.

Direction

side="left" and side="right" are physical, not logical: a left-side sidebar stays on the left in an RTL locale. That is deliberate — some products anchor navigation to a physical edge — but it means an RTL layout usually wants side="right" so the nav sits at the start of the line.

SidebarTrigger draws a PanelLeft glyph, which the RTL icon audit classifies as conditional for exactly this reason. Mirror it with rtl:-scale-x-100 when the sidebar follows reading direction; leave it alone when it is pinned to a physical side.

Accessibility

⌘/Ctrl+B toggles the sidebar from anywhere in the page. That is a global binding: if your app already uses it, override or remove it rather than shipping two handlers on one chord.

Below the mobile breakpoint the sidebar becomes a sheet, which traps focus and closes on Escape. Above it, the sidebar is ordinary page content and does not trap focus — a collapsed-to-icon sidebar still has its links in the tab order.

Wrap the nav items in a nav with an aria-label, and mark the current page with aria-current="page". SidebarMenuButton has an isActive prop, but that is styling — it sets a data attribute and no ARIA.

API Reference

SidebarProvider owns the open state; everything else reads it from context. The remaining parts — groups, menus, badges, skeletons, the rail — take plain element props and are best read from the anatomy above.

PropTypeDefaultDescription
defaultOpenbooleantrueInitial open state when uncontrolled.
openbooleanControlled open state. Pair with onOpenChange.
onOpenChange(open: boolean) => voidCalled when the open state should change.

Sidebar

PropTypeDefaultDescription
side"left" | "right""left"Which edge the sidebar is anchored to.
variant"sidebar" | "floating" | "inset""sidebar"Visual treatment of the sidebar surface.
collapsible"offcanvas" | "icon" | "none""offcanvas"How the sidebar collapses: slide off-canvas, shrink to icons, or never.