Sidebar
The sidebar shell. Reads open/collapsed state from {@link useSidebar}, so it
must render inside a {@link SidebarProvider}.
Adapts by viewport and collapsible: an animated docked shell on desktop, a
slide-in Sheet on mobile, or a static column when collapsible="none".
Harbour Street lobby
Materials shortlisted for the ground-floor refit.
Sample queue
Brushed brass mesh
Shipped
Limewash plaster, Dune
Packing
Oiled walnut veneer
Requested
Usage
@instruments/materia-ui/compositions/sidebar
<SidebarProvider
className="h-full min-h-0 overflow-hidden"
skipLink={false}
>
<AppSidebar onNavigate={setRoute} route={route} />
{/* The docs page owns the <main>, so this inset renders a div. */}
<SidebarInset
className="min-h-0 overflow-hidden"
id="sidebar-demo-inset"
render={<Box />}
>
<Flex
align="center"
as="header"
className="bg-background h-14 shrink-0 px-4"
gap="xs"
>
<SidebarTrigger />
<Separator
className="data-[orientation=vertical]:h-4"
orientation="vertical"
/>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="max-sm:hidden">
<BreadcrumbLink
href="#/projects"
onClick={(event) => {
event.preventDefault();
setRoute("/projects");
}}
>
Projects
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="max-sm:hidden" />
<BreadcrumbItem className="min-w-0">
<BreadcrumbCurrent className="truncate">
Harbour Street lobby
</BreadcrumbCurrent>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<Group className="ml-auto" gap="xs">
<Button
aria-label="Toggle sample queue"
aria-pressed={panelOpen}
emphasis="ghost"
icon={PanelRight}
onClick={() => {
setPanelOpen((open) => !open);
}}
size="icon-base"
/>
<Button icon={Plus} size="sm">
<Box as="span" className="max-sm:sr-only">
New project
</Box>
</Button>
</Group>
</Flex>
<Separator aria-hidden />
<Box className="min-h-0 flex-1 overflow-y-auto">
<Stack className="p-6" gap="xl">
<SectionHeader
description="Materials shortlisted for the ground-floor refit."
level="2"
size="heading-sm"
title="Harbour Street lobby"
/>
<SimpleGrid cols={2} gap="base">
{PROJECTS.map((project) => (
<Card key={project.title}>
<CardHeader>
<CardTitle>{project.title}</CardTitle>
<CardDescription>{project.description}</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground text-paragraph-sm">
{project.updated}
</CardContent>
</Card>
))}
</SimpleGrid>
</Stack>
</Box>
</SidebarInset>
<SidebarPanel
className="absolute! top-14! right-0 bottom-0 z-20 self-stretch shadow-lg"
open={panelOpen}
side="right"
width={288}
>
<Flex className="bg-background h-full w-72">
<Separator aria-hidden orientation="vertical" />
<Stack className="min-w-0 flex-1" gap="none">
<Flex align="center" className="h-14 shrink-0 px-4">
<Text weight="semibold">Sample queue</Text>
</Flex>
<Separator aria-hidden />
<Stack className="p-4" gap="base">
{SAMPLE_QUEUE.map((sample) => (
<PanelRow key={sample.label} {...sample} />
))}
</Stack>
</Stack>
</Flex>
</SidebarPanel>
</SidebarProvider>Best practices
Render
Sidebarinside aSidebarProvider- it reads open/collapsed state fromuseSidebarand cannot work outside one.Choose
collapsibledeliberately:offcanvasslides fully away,iconshrinks to a rail,noneis a static column that never adapts to the mobileSheet.
Props
Sidebar
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | - | |
| header | React.ReactNode | - | The brand row, lifted out of the sidebar into the shell's chrome layer on
desktop: it sits above the shell's overlays (a mega menu, a search
panel), which cover the sidebar's nav beneath it. It tracks the sidebar's
width and collapse. On mobile and in a static sidebar it heads the column
as an ordinary first child would. Pass a |
| side | "left" | "right" | "left" | Edge the sidebar docks to. |
| variant | "inset" | "sidebar" | "floating" | "sidebar" | Shell style: standard, floating (inset card), or inset (offset content). |
| collapsible | "none" | "icon" | "offcanvas" | "offcanvas" | Collapse behavior: slide off-canvas, shrink to icon rail, or never collapse. |
| resizable | boolean | false | Show a drag handle on the sidebar's inner edge to resize its expanded width (desktop only). Width is clamped, cookie-persisted, and dragging past the collapse threshold magnetically snaps to collapsed. |
SidebarGroupAction
| Prop | Type | Default | Description |
|---|---|---|---|
| tone | "neutral" | "brand" | "danger" | null | undefined | - | |
| radius | "rounded" | "square" | "full" | null | undefined | - | |
| square | boolean | null | undefined | - | |
| emphasis | "link" | "outline" | "solid" | "ghost" | "soft" | "transparent" | null | undefined | - | |
| asInput | boolean | null | undefined | - | |
| align | "center" | "end" | "start" | null | undefined | - | |
| pressed | boolean | null | undefined | - | |
| size | ButtonSizeProp | "base" | Size token; |
| render | useRender.RenderProp<Record<string, unknown>> | undefined | - | Replace the rendered |
| loading | boolean | undefined | false | Show a spinner and block presses. The button stays focusable (it is
|
| loadingLabel | string | undefined | - | Label shown in place of |
| icon | IconComponent | undefined | - | Leading icon (Lucide or SVG component); with no children it becomes an icon-only button. |
| iconClassName | string | undefined | - | Extra classes for the leading icon. |
| suffixIcon | IconComponent | undefined | - | Trailing icon (Lucide or SVG component) rendered at the end. |
| suffixIconClassName | string | undefined | - | Extra classes for the trailing icon. |
| dot | string | undefined | - | CSS colour for a status dot; empty/undefined renders no dot. |
| dotPlacement | "end" | "start" | undefined | "start" | Dot position relative to content. |
| expandable | boolean | undefined | false | Icon-only at rest; expands on hover/focus-visible to reveal the label
to the right of the icon. Collapsed geometry equals the matching icon-*
size, expanded equals the text button of that size - so pass a text
size ( |
| focused | boolean | undefined | - | Force the focus ring on - for documentation/testing only. |
| hovered | boolean | undefined | - | Force the hover state on - for documentation/testing only. |
| testId | string | - | |
| showWhenCollapsed | boolean | - | Show the action button even when sidebar is collapsed |
SidebarInput
| Prop | Type | Default | Description |
|---|---|---|---|
| testId | string | - | |
| radius | "rounded" | "square" | "full" | null | undefined | - | |
| size | ResponsiveSize | "xl" (48px) | Field size - a single token or a per-breakpoint object like
|
| hasError | boolean | - | Force error styling; overrides invalid state inherited from an ancestor Field. |
| focused | boolean | - | Force focus ring display (for documentation/testing) |
SidebarInset
| Prop | Type | Default | Description |
|---|---|---|---|
| offset | "header" | "none" | "app-bar" | "none" | Pad under fixed chrome in |
| gutter | boolean | false | Apply the shared horizontal page gutter ( |
SidebarLayoutHeader
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "fixed" | "sticky" | "fixed" |
|
| gutter | boolean | false | Apply the shared page gutter ( |
| rightOffset | string | - | Right padding when sidepanel or other overlay is open ( |
SidebarMenuAction
| Prop | Type | Default | Description |
|---|---|---|---|
| showOnHover | boolean | false | Reveal only on row hover/focus (or when open) instead of always. |
SidebarMenuButton
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | - | Marks the row as the current/selected item (sets |
| tooltip | string | (Omit<import("@base-ui/react").TooltipPopupProps, "className"> & { anchor?: Element | import("@floating-ui/dom").VirtualElement | React.RefObject<Element | null> | (() => Element | import("@f | - | Tooltip content shown only when the sidebar is collapsed; also becomes the collapsed |
| tooltipOpen | boolean | - | Control tooltip visibility. Use |
| size | "base" | "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "3xl" | - | |
| variant | "input" | "default" | "outline" | - | |
| icon | IconComponent | - | |
| collapsedIcon | IconComponent | - | Icon to show when sidebar is collapsed (overrides icon) |
| collapsedContent | React.ReactNode | - | Custom content to show when sidebar is collapsed (e.g., Avatar). Takes precedence over collapsedIcon. |
| suffixIcon | IconComponent | - | |
| forceExpanded | boolean | - | Keep label/suffix visible even when the sidebar provider is collapsed. |
| kbd | string[] | - | |
| children | React.ReactNode | - |
SidebarMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
| delay | number | 0 | Delay (seconds) before the expand/collapse fade, for staggering labels. |
SidebarMenuSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
| showIcon | boolean | false | Also render a leading icon placeholder. |
| index | number | - | Index for deterministic width (prevents SSR hydration mismatch) |
SidebarMenuSubButton
| Prop | Type | Default | Description |
|---|---|---|---|
| kbd | string[] | undefined | - | Keyboard shortcut keys to display (e.g., ["⌘", "C"]). Ignored if suffixIcon is provided. |
| children | string | number | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | undefined | - | Text label for the menu item. Use |
| render | useRender.RenderProp<Record<string, unknown>> | undefined | - | Replace the default |
| variant | "input" | "default" | "outline" | null | undefined | - | |
| indicator | React.ReactNode | - | Indicator element for checkbox/radio items (absolutely positioned on left). Automatically enables inset. |
| tone | "neutral" | "danger" | null | undefined | - | |
| inset | boolean | null | undefined | - | |
| radius | "rounded" | "square" | "full" | null | undefined | - | |
| size | "base" | "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "3xl" | null | undefined | - | |
| loading | boolean | undefined | - | Show loading spinner in place of icon |
| active | boolean | undefined | - | Whether the item is in an active/selected state |
| checked | boolean | undefined | - | Checked state. Renders the selectionMode indicator inline. When undefined, no indicator renders. |
| suffixIcon | LucideIcon | React.ComponentType<React.SVGProps<SVGSVGElement>> | undefined | - | Icon to display at the end (e.g., checkmark, chevron). Takes precedence over kbd. |
| selectionMode | "checkbox" | "radio" | undefined | - | Selection visual: "checkbox" renders Checkbox, "radio" renders radio circle. Defaults to "checkbox" when |
| iconClassName | string | - | Additional className for the icon |
| avatar | React.ReactNode | - | Avatar element to display at the start (takes precedence over icon and dot) |
| activeIndicator | "none" | "dot" | "check" | undefined | - | Visual indicator for active state. "dot" shows grey dot normally, green when active. "check" shows green checkmark only when active. |
| kbdTone | "neutral" | "brand" | "danger" | null | undefined | - | Tone for kbd elements. Options: "neutral" (default), "brand", "danger". |
| kbdEmphasis | "outline" | "solid" | "ghost" | "soft" | null | undefined | - | Emphasis for kbd elements. Options: "solid" (default), "soft", "outline", "ghost". |
| suffix | React.ReactNode | - | Content to display at the end after suffixIcon/kbd (e.g., custom content) |
| dot | boolean | "neutral" | "brand" | "danger" | "warning" | "success" | "info" | - | Dot indicator in the left column (takes precedence over icon): |
| icon | IconComponent | - | Icon to display in the left icon column |
SidebarMenuSubScrollArea
| Prop | Type | Default | Description |
|---|---|---|---|
| fade | boolean | false | Add a bottom gradient fade hinting at scrollable overflow. |
| maxHeight | "none" | "dropdown" | "none" | Cap the height: unconstrained, or clamped to the shared dropdown max. |
SidebarPanel
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "left" | "right" | "right" | Which edge the panel docks to. Positioning comes from DOM order (place the
panel before or after |
| variant | "inline" | "gutter" | "inline" |
|
| open | boolean | true |
|
| width | string | number | - | Panel width - sets |
SidebarProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultOpen | boolean | true | Initial open state when uncontrolled. |
| defaultWidth | number | 256 | Initial expanded width in px, typically read from the width cookie server-side so the resized width is applied before paint (no flash). |
| height | "fill" | "auto" | "auto" | Scroll model. |
| skipLink | boolean | true | Render the skip-to-content link first, targeting |
| skipLinkLabel | string | "Skip to content" | Skip-link text. |
| open | boolean | - | Controlled open state - pair with |
| onOpenChange | (open: boolean) => void | - | Called with the next open state on toggle; required for controlled use. |
SidebarSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "strong" | "default" | "muted" | "accent" | "default" | Visual weight of the separator line. |
| showBorder | boolean | - | Whether to show the border line. Defaults to true. |
SidebarSkipLink
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Skip to content" | Link text. |
| targetId | string | "main-content" | Id of the target |
SidebarTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| render | useRender.RenderProp<Record<string, unknown>> | undefined | - | Replace the rendered |
| tone | "neutral" | "brand" | "danger" | null | undefined | - | |
| radius | "rounded" | "square" | "full" | null | undefined | - | |
| size | ButtonSizeProp | "base" | Size token; |
| dot | string | undefined | - | CSS colour for a status dot; empty/undefined renders no dot. |
| loading | boolean | undefined | false | Show a spinner and block presses. The button stays focusable (it is
|
| square | boolean | null | undefined | - | |
| emphasis | "link" | "outline" | "solid" | "ghost" | "soft" | "transparent" | null | undefined | - | |
| asInput | boolean | null | undefined | - | |
| align | "center" | "end" | "start" | null | undefined | - | |
| pressed | boolean | null | undefined | - | |
| testId | string | - | |
| suffixIcon | IconComponent | undefined | - | Trailing icon (Lucide or SVG component) rendered at the end. |
| expandable | boolean | undefined | false | Icon-only at rest; expands on hover/focus-visible to reveal the label
to the right of the icon. Collapsed geometry equals the matching icon-*
size, expanded equals the text button of that size - so pass a text
size ( |
| focused | boolean | undefined | - | Force the focus ring on - for documentation/testing only. |
| hovered | boolean | undefined | - | Force the hover state on - for documentation/testing only. |
| loadingLabel | string | undefined | - | Label shown in place of |
| iconClassName | string | undefined | - | Extra classes for the leading icon. |
| suffixIconClassName | string | undefined | - | Extra classes for the trailing icon. |
| dotPlacement | "end" | "start" | undefined | "start" | Dot position relative to content. |
| icon | IconComponent | - | Icon to display. Defaults to PanelLeftCloseIcon. |
Also exports
- const
CONTENT_COLUMN_CLASS - const
HEADER_HEIGHT - const
SIDEBAR_COOKIE_MAX_AGE - const
SIDEBAR_COOKIE_NAME - const
SIDEBAR_INSET_GAP - const
SIDEBAR_INSET_ID - const
SIDEBAR_KEYBOARD_SHORTCUT - const
SIDEBAR_MOBILE_BREAKPOINT_PX - const
SIDEBAR_TRANSITION - const
SIDEBAR_WIDTH - const
SIDEBAR_WIDTH_COOKIE_NAME - const
SIDEBAR_WIDTH_DEFAULT_PX - const
SIDEBAR_WIDTH_ICON - const
SIDEBAR_WIDTH_MAX_PX - const
SIDEBAR_WIDTH_MIN_PX - const
SIDEBAR_WIDTH_MOBILE - const
sidebarAnatomy - component
SidebarContent - component
SidebarFooter - component
SidebarGroup - component
SidebarGroupContent - component
SidebarGroupLabel - component
SidebarHeader - component
SidebarLayoutHeaderActions - component
SidebarLayoutHeaderSearch - component
SidebarMenu - component
SidebarMenuBadge - component
SidebarMenuItem - component
SidebarMenuSub - component
SidebarMenuSubItem - type
SidebarPanelProps - component
SidebarRail - const
useSidebar - const
useSidebarResize
Anatomy
SidebarProvider
sidebar-wrapperFrame root: sidebar state, the auto|fill height model and the skip link
SidebarSkipLink
sidebar-skip-linkBypass-blocks link to the main content (WCAG 2.4.1)
Sidebar
sidebarThe collapsible navigation panel
SidebarHeader
sidebar-headerTop region for branding or search
SidebarContent
sidebar-contentScrollable main region holding menus and groups
SidebarMenu
sidebar-menuList of navigation entries
SidebarMenuItem
sidebar-menu-itemSingle menu entry
SidebarMenuButton
sidebar-menu-buttonInteractive navigation link
SidebarFooter
sidebar-footerBottom region for account or secondary actions
SidebarTrigger
sidebar-triggerToggle that opens and closes the sidebar
SidebarInset
sidebar-insetThe content column and the page's single <main> landmark
SidebarPanel
sidebar-panelRegion reserving a docked-overlay gutter (variant="gutter") or an in-flow sticky column (variant="inline")