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
SidebarTriggerSidebarInset (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
└── SidebarTriggerWhen 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.
| Prop | Type | Default | Description |
|---|---|---|---|
toggleSidebar | string | "Toggle Sidebar" | Accessible name for SidebarTrigger, whose visible content is an icon. |
mobileTitle | string | "Sidebar" | Title of the mobile sheet. Required by the dialog underneath, and visually hidden. |
mobileDescription | string | "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.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | true | Initial open state when uncontrolled. |
open | boolean | — | Controlled open state. Pair with onOpenChange. |
onOpenChange | (open: boolean) => void | — | Called when the open state should change. |
Sidebar
| Prop | Type | Default | Description |
|---|---|---|---|
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. |