Skip to content
Compositions

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

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 Sidebar inside a SidebarProvider - it reads open/collapsed state from useSidebar and cannot work outside one.

  • Choose collapsible deliberately: offcanvas slides fully away, icon shrinks to a rail, none is a static column that never adapts to the mobile Sheet.

Props

Sidebar

PropTypeDefaultDescription
childrenReact.ReactNode
headerReact.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 SidebarHeader.

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.

resizablebooleanfalse

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

PropTypeDefaultDescription
tone"neutral" | "brand" | "danger" | null | undefined
radius"rounded" | "square" | "full" | null | undefined
squareboolean | null | undefined
emphasis"link" | "outline" | "solid" | "ghost" | "soft" | "transparent" | null | undefined
asInputboolean | null | undefined
align"center" | "end" | "start" | null | undefined
pressedboolean | null | undefined
sizeButtonSizeProp"base"

Size token; icon-* variants produce a square icon-only button. Also accepts a responsive object keyed by viewport breakpoints and/or @-prefixed container queries - e.g. { base: "sm", md: "base", "@sm": "lg" }.

renderuseRender.RenderProp<Record<string, unknown>> | undefined

Replace the rendered <button> with another element or component (e.g. <Link href="…" />); styling, decorations and props are applied to it.

loadingboolean | undefinedfalse

Show a spinner and block presses. The button stays focusable (it is aria-disabled, not natively disabled) so keyboard focus is kept.

loadingLabelstring | undefined

Label shown in place of children while loading.

iconIconComponent | undefined

Leading icon (Lucide or SVG component); with no children it becomes an icon-only button.

iconClassNamestring | undefined

Extra classes for the leading icon.

suffixIconIconComponent | undefined

Trailing icon (Lucide or SVG component) rendered at the end.

suffixIconClassNamestring | undefined

Extra classes for the trailing icon.

dotstring | undefined

CSS colour for a status dot; empty/undefined renders no dot.

dotPlacement"end" | "start" | undefined"start"

Dot position relative to content.

expandableboolean | undefinedfalse

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 (sm/base/lg) OR its icon twin (icon-sm …); both resolve identically. Requires both icon and children; ignored with square. The label stays in the DOM in both states (stable accessible name).

focusedboolean | undefined

Force the focus ring on - for documentation/testing only.

hoveredboolean | undefined

Force the hover state on - for documentation/testing only.

testIdstring
showWhenCollapsedboolean

Show the action button even when sidebar is collapsed

SidebarInput

PropTypeDefaultDescription
testIdstring
radius"rounded" | "square" | "full" | null | undefined
sizeResponsiveSize"xl" (48px)

Field size - a single token or a per-breakpoint object like { base: "sm", md: "lg" }.

hasErrorboolean

Force error styling; overrides invalid state inherited from an ancestor Field.

focusedboolean

Force focus ring display (for documentation/testing)

SidebarInset

PropTypeDefaultDescription
offset"header" | "none" | "app-bar""none"

Pad under fixed chrome in height="auto" frames. "header" clears the fixed SidebarLayoutHeader (and the mobile app bar below md); "app-bar" clears only the mobile app bar, for frames whose desktop header is a sticky SidebarLayoutHeader inside the inset.

gutterbooleanfalse

Apply the shared horizontal page gutter (--page-gutter). Opt-in so apps that own their own gutter are unaffected.

SidebarLayoutHeader

PropTypeDefaultDescription
position"fixed" | "sticky""fixed"

"fixed" pins the bar to the viewport and animates its left edge to track the sidebar. "sticky" renders it in flow as the first child of SidebarInset: it inherits the column's width, so it follows the sidebar and any SidebarPanel gutter by layout alone, and works the same in height="auto" and height="fill" frames.

gutterbooleanfalse

Apply the shared page gutter (--page-gutter) on both sides, so the bar's contents line up with a PageHeader and page content below it.

rightOffsetstring

Right padding when sidepanel or other overlay is open (fixed only).

SidebarMenuAction

PropTypeDefaultDescription
showOnHoverbooleanfalse

Reveal only on row hover/focus (or when open) instead of always.

SidebarMenuButton

PropTypeDefaultDescription
activeboolean

Marks the row as the current/selected item (sets data-active).

tooltipstring | (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 aria-label.

tooltipOpenboolean

Control tooltip visibility. Use false to hide tooltip (e.g., when a dropdown is open).

size"base" | "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "3xl"
variant"input" | "default" | "outline"
iconIconComponent
collapsedIconIconComponent

Icon to show when sidebar is collapsed (overrides icon)

collapsedContentReact.ReactNode

Custom content to show when sidebar is collapsed (e.g., Avatar). Takes precedence over collapsedIcon.

suffixIconIconComponent
forceExpandedboolean

Keep label/suffix visible even when the sidebar provider is collapsed.

kbdstring[]
childrenReact.ReactNode

SidebarMenuLabel

PropTypeDefaultDescription
delaynumber0

Delay (seconds) before the expand/collapse fade, for staggering labels.

SidebarMenuSkeleton

PropTypeDefaultDescription
showIconbooleanfalse

Also render a leading icon placeholder.

indexnumber

Index for deterministic width (prevents SSR hydration mismatch)

SidebarMenuSubButton

PropTypeDefaultDescription
kbdstring[] | undefined

Keyboard shortcut keys to display (e.g., ["⌘", "C"]). Ignored if suffixIcon is provided.

childrenstring | number | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | undefined

Text label for the menu item. Use icon prop for icons, not children.

renderuseRender.RenderProp<Record<string, unknown>> | undefined

Replace the default <button> with another element or component. Glyph props render inside it, around children; the element should carry no children of its own.

variant"input" | "default" | "outline" | null | undefined
indicatorReact.ReactNode

Indicator element for checkbox/radio items (absolutely positioned on left). Automatically enables inset.

tone"neutral" | "danger" | null | undefined
insetboolean | null | undefined
radius"rounded" | "square" | "full" | null | undefined
size"base" | "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "3xl" | null | undefined
loadingboolean | undefined

Show loading spinner in place of icon

activeboolean | undefined

Whether the item is in an active/selected state

checkedboolean | undefined

Checked state. Renders the selectionMode indicator inline. When undefined, no indicator renders.

suffixIconLucideIcon | 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 checked is set.

iconClassNamestring

Additional className for the icon

avatarReact.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".

suffixReact.ReactNode

Content to display at the end after suffixIcon/kbd (e.g., custom content)

dotboolean | "neutral" | "brand" | "danger" | "warning" | "success" | "info"

Dot indicator in the left column (takes precedence over icon): true for a neutral dot, or a {@link Tone} to tint it.

iconIconComponent

Icon to display in the left icon column

SidebarMenuSubScrollArea

PropTypeDefaultDescription
fadebooleanfalse

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

PropTypeDefaultDescription
side"left" | "right""right"

Which edge the panel docks to. Positioning comes from DOM order (place the panel before or after SidebarInset in the row); this only sets data-side for styling hooks.

variant"inline" | "gutter""inline"

"inline": an in-flow, sticky column that renders its children. "gutter": an empty, width-animated spacer that reserves room for a docked overlay (children are dropped and it is aria-hidden).

openbooleantrue

gutter animates its width between 0 and the panel width (lg: 300ms ease-out); inline is hidden when closed.

widthstring | number

Panel width - sets --sidebar-panel-width (a number is treated as px). Falls back to var(--sidepanel-footprint) (464px) for gutter and 25rem for inline.

SidebarProvider

PropTypeDefaultDescription
defaultOpenbooleantrue

Initial open state when uncontrolled.

defaultWidthnumber256

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. "auto" grows with the page (min-h-svh, like shadcn); "fill" pins the frame to the viewport and SidebarInset scrolls internally.

skipLinkbooleantrue

Render the skip-to-content link first, targeting SidebarInset. Set false when the app root already renders one SidebarSkipLink.

skipLinkLabelstring"Skip to content"

Skip-link text.

openboolean

Controlled open state - pair with onOpenChange.

onOpenChange(open: boolean) => void

Called with the next open state on toggle; required for controlled use.

SidebarSeparator

PropTypeDefaultDescription
variant"strong" | "default" | "muted" | "accent""default"

Visual weight of the separator line.

showBorderboolean

Whether to show the border line. Defaults to true.

SidebarSkipLink

PropTypeDefaultDescription
labelstring"Skip to content"

Link text.

targetIdstring"main-content"

Id of the target <main>.

SidebarTrigger

PropTypeDefaultDescription
renderuseRender.RenderProp<Record<string, unknown>> | undefined

Replace the rendered <button> with another element or component (e.g. <Link href="…" />); styling, decorations and props are applied to it.

tone"neutral" | "brand" | "danger" | null | undefined
radius"rounded" | "square" | "full" | null | undefined
sizeButtonSizeProp"base"

Size token; icon-* variants produce a square icon-only button. Also accepts a responsive object keyed by viewport breakpoints and/or @-prefixed container queries - e.g. { base: "sm", md: "base", "@sm": "lg" }.

dotstring | undefined

CSS colour for a status dot; empty/undefined renders no dot.

loadingboolean | undefinedfalse

Show a spinner and block presses. The button stays focusable (it is aria-disabled, not natively disabled) so keyboard focus is kept.

squareboolean | null | undefined
emphasis"link" | "outline" | "solid" | "ghost" | "soft" | "transparent" | null | undefined
asInputboolean | null | undefined
align"center" | "end" | "start" | null | undefined
pressedboolean | null | undefined
testIdstring
suffixIconIconComponent | undefined

Trailing icon (Lucide or SVG component) rendered at the end.

expandableboolean | undefinedfalse

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 (sm/base/lg) OR its icon twin (icon-sm …); both resolve identically. Requires both icon and children; ignored with square. The label stays in the DOM in both states (stable accessible name).

focusedboolean | undefined

Force the focus ring on - for documentation/testing only.

hoveredboolean | undefined

Force the hover state on - for documentation/testing only.

loadingLabelstring | undefined

Label shown in place of children while loading.

iconClassNamestring | undefined

Extra classes for the leading icon.

suffixIconClassNamestring | undefined

Extra classes for the trailing icon.

dotPlacement"end" | "start" | undefined"start"

Dot position relative to content.

iconIconComponent

Icon to display. Defaults to PanelLeftCloseIcon.

Also exports

  • constCONTENT_COLUMN_CLASS
  • constHEADER_HEIGHT
  • constSIDEBAR_COOKIE_MAX_AGE
  • constSIDEBAR_COOKIE_NAME
  • constSIDEBAR_INSET_GAP
  • constSIDEBAR_INSET_ID
  • constSIDEBAR_KEYBOARD_SHORTCUT
  • constSIDEBAR_MOBILE_BREAKPOINT_PX
  • constSIDEBAR_TRANSITION
  • constSIDEBAR_WIDTH
  • constSIDEBAR_WIDTH_COOKIE_NAME
  • constSIDEBAR_WIDTH_DEFAULT_PX
  • constSIDEBAR_WIDTH_ICON
  • constSIDEBAR_WIDTH_MAX_PX
  • constSIDEBAR_WIDTH_MIN_PX
  • constSIDEBAR_WIDTH_MOBILE
  • constsidebarAnatomy
  • componentSidebarContent
  • componentSidebarFooter
  • componentSidebarGroup
  • componentSidebarGroupContent
  • componentSidebarGroupLabel
  • componentSidebarHeader
  • componentSidebarLayoutHeaderActions
  • componentSidebarLayoutHeaderSearch
  • componentSidebarMenu
  • componentSidebarMenuBadge
  • componentSidebarMenuItem
  • componentSidebarMenuSub
  • componentSidebarMenuSubItem
  • typeSidebarPanelProps
  • componentSidebarRail
  • constuseSidebar
  • constuseSidebarResize

Anatomy

SidebarProvider

sidebar-wrapper

Frame root: sidebar state, the auto|fill height model and the skip link

SidebarSkipLink

sidebar-skip-link

Bypass-blocks link to the main content (WCAG 2.4.1)

Sidebar

sidebar

The collapsible navigation panel

SidebarHeader

sidebar-header

Top region for branding or search

SidebarContent

sidebar-content

Scrollable main region holding menus and groups

SidebarMenu

sidebar-menu

List of navigation entries

SidebarMenuItem

sidebar-menu-item

Single menu entry

SidebarMenuButton

sidebar-menu-button

Interactive navigation link

SidebarFooter

sidebar-footer

Bottom region for account or secondary actions

SidebarTrigger

sidebar-trigger

Toggle that opens and closes the sidebar

SidebarInset

sidebar-inset

The content column and the page's single <main> landmark

SidebarPanel

sidebar-panel

Region reserving a docked-overlay gutter (variant="gutter") or an in-flow sticky column (variant="inline")