Sidebar

The collapsible left-side navigation bar for listing nested pages and guides.

The Sidebar component displays the site directory structure, listing nested folders, page routes, category headers, and navigation labels.

The Sidebar primitive is style-neutral: it provides structure, accessibility (ARIA), behavior (scroll restore, auto-open active groups, scroll-to-active) and a set of data-* state attributes. Every visual decision belongs to your theme — colors, spacing, borders, active/hover treatments. Style states with Tailwind v4 variants (data-active:bg-primary-500/10) or plain CSS ([data-active] { ... }).

Composable Sidebar ExampleLink

Build a custom responsive sidebar using the primitive containers and the automatic routing hook:

tsx
// docs/components/CustomSidebar.tsx
import { Sidebar } from 'boltdocs/primitives'
import { useRoutes } from 'boltdocs/client'

export default function CustomSidebar() {
  const { routes } = useRoutes()

  return (
    <>
      {/* 1. Desktop Sidebar Container */}
      <Sidebar className="hidden lg:flex flex-col w-sidebar sticky top-navbar h-[calc(100vh-var(--spacing-navbar))] border-r border-subtle bg-main">
        <Sidebar.Header className="px-4 py-3 flex items-center justify-between border-b border-subtle">
          <span className="text-xs font-bold uppercase tracking-wider text-muted">Navigation</span>
        </Sidebar.Header>
        <Sidebar.Content className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
          {/* Active items are exposed via data-active; style them in your theme. */}
          <Sidebar.Items
            routes={routes}
            classNames={{
              item: 'rounded-md px-2.5 py-1.5 text-sm transition-colors text-muted hover:bg-surface hover:text-body data-active:bg-primary-500/10 data-active:text-primary-500 data-active:font-medium',
              groupHeader: 'px-2 mb-2 text-xs font-bold uppercase tracking-widest text-muted/50',
              groupContent: 'flex flex-col gap-0.5',
              subgroupContent: 'ml-4 pl-3 border-l border-subtle/50 mt-0.5 flex flex-col gap-0.5',
            }}
          />
        </Sidebar.Content>
      </Sidebar>

      {/* 2. Responsive Mobile Drawer Container */}
      <Sidebar.Mobile
        overlayClassName="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm lg:hidden"
        className="fixed top-0 left-0 bottom-0 w-80 bg-main border-r border-subtle shadow-2xl outline-none"
      >
        <Sidebar.Header className="px-4 py-3 flex items-center justify-between border-b border-subtle">
          <span className="text-xs font-bold uppercase tracking-wider text-muted">Menu</span>
        </Sidebar.Header>
        <Sidebar.Content className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
          <Sidebar.Items routes={routes} />
        </Sidebar.Content>
      </Sidebar.Mobile>
    </>
  )
}

Real-World Production ReferenceLink

Below is a complete, production-ready reference implementation demonstrating how to build a fully-featured sidebar navigation system (similar to the built-in theme sidebar). It integrates logo branding, mobile sidebar close controls, desktop vs. mobile structures, and combines version and translation dropdowns inside the mobile menu drawer:

tsx
// docs/components/ProductionSidebar.tsx
import { Sidebar, Button, Menu } from 'boltdocs/primitives'
import { useRoutes, useNavbar, useUI, useVersion, useI18n } from 'boltdocs/client'
import { X, ChevronDown, Languages } from 'lucide-react'

export default function ProductionSidebar() {
  const { routes, config } = useRoutes()
  const { logo, title, logoProps } = useNavbar()
  const { closeSidebar } = useUI()

  const { currentVersionLabel, availableVersions, handleVersionChange } = useVersion()
  const { currentLocale, availableLocales, handleLocaleChange } = useI18n()

  const SidebarLogo = logo ? (
    <img
      src={logo}
      alt={logoProps?.alt || title}
      width={24}
      height={24}
      className="rounded-lg"
    />
  ) : null

  // 1. Version Switch Selector for Mobile Layouts
  const MobileVersionSelector = availableVersions.length > 0 && (
    <select
      value={availableVersions.find(v => v.isCurrent)?.value}
      onChange={(e) => handleVersionChange(e.target.value)}
      className="flex-1 bg-surface border border-subtle rounded-lg px-3 py-2 text-xs font-semibold"
    >
      {availableVersions.map(v => (
        <option key={v.key} value={v.value}>{v.label}</option>
      ))}
    </select>
  )

  // 2. Language Selector for Mobile Layouts
  const MobileLocaleSelector = availableLocales.length > 0 && (
    <select
      value={currentLocale}
      onChange={(e) => handleLocaleChange(e.target.value)}
      className="flex-1 bg-surface border border-subtle rounded-lg px-3 py-2 text-xs font-semibold"
    >
      {availableLocales.map(loc => (
        <option key={loc.key} value={loc.value}>{loc.label}</option>
      ))}
    </select>
  )

  const hasMobileSelectors = availableVersions.length > 0 || availableLocales.length > 0

  return (
    <>
      {/* A. Desktop Sidebar Panel */}
      <Sidebar className="hidden lg:flex flex-col w-sidebar sticky top-navbar h-[calc(100vh-var(--spacing-navbar))] border-r border-subtle bg-main">
        <Sidebar.Content className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
          <Sidebar.Items
            routes={routes}
            classNames={{
              item: 'rounded-md px-2.5 py-1.5 text-sm transition-colors text-muted hover:bg-surface hover:text-body data-active:bg-primary-500/10 data-active:text-primary-500',
            }}
          />
        </Sidebar.Content>
      </Sidebar>

      {/* B. Mobile Drawer Panel */}
      <Sidebar.Mobile
        overlayClassName="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm lg:hidden"
        className="fixed top-0 left-0 bottom-0 w-80 bg-main border-r border-subtle shadow-2xl outline-none"
      >
        <Sidebar.Header className="flex items-center justify-between border-b border-subtle px-4 py-3">
          <div className="flex items-center gap-3">
            {SidebarLogo}
            <span className="font-bold text-base text-body truncate max-w-[140px]">
              {title}
            </span>
          </div>

          <Button
            onPress={closeSidebar}
            className="h-8 w-8 flex items-center justify-center text-muted hover:text-body rounded-lg hover:bg-surface transition-colors"
          >
            <X size={18} />
          </Button>
        </Sidebar.Header>

        <Sidebar.Content className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
          {/* Version/Language dropdowns embedded inside the mobile drawer */}
          {hasMobileSelectors && (
            <div className="flex flex-col gap-3 mb-6">
              <div className="flex gap-2">
                {MobileVersionSelector}
                {MobileLocaleSelector}
              </div>
              <div className="border-b border-subtle my-2" />
            </div>
          )}

          <Sidebar.Items routes={routes} />
        </Sidebar.Content>
      </Sidebar.Mobile>
    </>
  )
}

State Attributes (data-*)Link

The Sidebar primitives expose state exclusively through data-* attributes. They are present only when true, so presence-based CSS works out of the box. Decide your own colors (primary, secondary, success, ...) from CSS — the primitive never paints anything itself.

AttributeEmitted OnPresent When
data-activeSidebar.Link anchor, Sidebar.Group containerThe link / group contains the current route
data-openCollapsible group button + chevron, Sidebar.SubGroup toggle + chevronThe section is expanded
data-collapsibleSidebar.Group containerThe group renders a toggle button
data-depthSidebar.Link anchorNesting depth (number)
data-badgeBadge spanBadge text / type (e.g. new, updated, deprecated)
aria-currentSidebar.Link anchor"page" when active
aria-expandedToggle buttons"true" / "false"

Styling the active item from your theme — Tailwind v4:

tsx
<Sidebar.Items
  routes={routes}
  classNames={{
    item: 'rounded-md px-2.5 py-1.5 text-sm transition-colors text-muted hover:bg-soft hover:text-body data-active:bg-primary-500/10 data-active:text-primary-400',
  }}
/>

Or vanilla CSS:

css
[data-sidebar-root] a[data-active] {
  color: var(--color-primary-400);
  background: color-mix(in srgb, var(--color-primary-500) 10%, transparent);
}

Icons rendered inside a link inherit currentColor, so coloring the anchor colors the icon automatically — no per-icon state classes needed.


Composable SubcomponentsLink

The Sidebar primitive provides the following subcomponents for structure customization:

ComponentHTML TagDescriptionProps
Sidebar.Root / Sidebar<aside>Main outer desktop sidebar container.ComponentBaseProps
Sidebar.Mobile<div> (Modal)Mobile sidebar drawer container using accessible overlays.ComponentBaseProps
Sidebar.Header<div>Padded top section container (ideal for logos, search inputs, or headers).ComponentBaseProps
Sidebar.Content<nav>Scrollable navigation wrapper preserving scroll positions.ComponentBaseProps
Sidebar.Group<div>Groups a set of related navigation links under a common title.Sidebar.Group Props
Sidebar.Link<a>An optimized navigation link to a specific page route.Sidebar.Link Props
Sidebar.SubGroup<div>A collapsible section container for nested sub-routes.Sidebar.SubGroup Props
Sidebar.ItemVariesRecursive router mapping utility rendering links or subgroups automatically.Sidebar.Item Props
Sidebar.ItemsVariesAutomated loop container mapping and rendering a collection of routes.Sidebar.Items Props

Component PropsLink

ComponentBaseProps (Common)Link

PropertyTypeDefaultDescription
childrenReactNodeundefinedChild content elements.
classNamestringundefinedCustom utility CSS classes.
styleCSSPropertiesundefinedInline style settings.

Sidebar.Mobile PropsLink

PropertyTypeDefaultDescription
overlayClassNamestringundefinedClasses for the full-screen overlay behind the drawer.

Sidebar.Group PropsLink

PropertyTypeDefaultDescription
titlestringundefinedGroup category title text.
iconElementTypeundefinedCustom leading icon component.
childrenReactNodeRequiredSidebar child elements.
collapsiblebooleanfalseRenders the header as a toggle button instead of a heading.
collapsedbooleanfalseInitial collapsed state for collapsible groups.
activebooleanfalseKeeps the group open when it contains the active route.
headerClassNamestringundefinedExtra classes for the group header (button or heading).
iconClassNamestringundefinedExtra classes for the leading group icon.
titleClassNamestringundefinedExtra classes for the title wrapper.
contentClassNamestringundefinedExtra classes for the children wrapper.
trailingReactNodeundefinedCustom content rendered at the end of the header.
renderTitle({ title, isOpen }) => ReactNodeundefinedFull replacement for the header content.
PropertyTypeDefaultDescription
labelstringRequiredDisplay name label of the link.
hrefstringRequiredLink route target path.
activebooleanfalseActive state — reflected via data-active + aria-current="page".
iconElementTypeundefinedLeading icon element wrapper.
badgestring | BadgeConfigundefinedNotification status label.
childrenReactNodeundefinedWhen provided, overrides the label content.
iconClassNamestringundefinedExtra classes for the leading icon.
labelClassNamestringundefinedExtra classes for the label text.
trailingReactNodeundefinedCustom content rendered after the label.
depthnumberundefinedNesting depth — reflected via data-depth.

Sidebar.SubGroup PropsLink

Inherits all Sidebar.Link props plus:

PropertyTypeDefaultDescription
isOpenbooleanfalseCollapsed state visibility indicator.
onToggle() => voidundefinedToggle trigger callback handler.
childrenReactNodeRequiredSubgroup navigation links.
rootClassNamestringundefinedExtra classes for the subgroup wrapper.
linkClassNamestringundefinedExtra classes for the subgroup link.
contentClassNamestringundefinedExtra classes for the nested children wrapper.
toggleClassNamestringundefinedExtra classes for the expand/collapse toggle.
renderToggle({ isOpen }) => ReactNodeundefinedFull replacement for the toggle button.

Sidebar.Item PropsLink

PropertyTypeDefaultDescription
routeComponentRouteRequiredRoute configuration object to parse.
activePathstringRequiredActive window URL path.
activeRouteComponentRouteundefinedThe resolved active route (enables file-path matching).
depthnumber0Current nesting depth, used by renderItem.
renderItem(route, depth) => ReactNodeundefinedRecursive render override for child routes.
classNamesSidebarSlotsundefinedPer-piece class overrides merged over the defaults.

Sidebar.Items PropsLink

PropertyTypeDefaultDescription
routesComponentRoute[]RequiredArray of route configurations to render recursively.
classNamesSidebarSlotsundefinedPer-piece class overrides merged over the defaults.
componentItem(props: SidebarItemRenderProps) => ReactNodeundefinedFull render replacement for a route node (runs recursively for nested routes).
componentGroup(props: SidebarGroupRenderProps) => ReactNodeundefinedFull render replacement for a top-level group wrapper.

Slots (classNames)Link

Sidebar.Items accepts a classNames map that merges over the primitive defaults using tailwind-merge, so theme overrides win without ! important markers. Each key maps to a single piece of the automated tree. The defaults are structural only — layout and states are yours to style:

KeyApplied ToDefault
itemLeaf route link rootgroup flex items-center gap-2.5 outline-none
itemIconLeaf route leading iconshrink-0
itemLabelLeaf route labeltruncate
groupGroup wrapperundefined
groupHeaderGroup header (button or heading)undefined
groupIconGroup leading iconundefined
groupTitleGroup title wrapperflex items-center gap-2
groupContentGroup children wrapperflex flex-col
subgroupSubgroup wrapperflex flex-col
subgroupLinkSubgroup linkflex-1 pr-8
subgroupContentSubgroup children wrapperflex flex-col
toggleSubgroup expand/collapse toggleabsolute right-1
tsx
<Sidebar.Items
  routes={routes}
  classNames={{
    item: 'rounded-md px-2.5 py-1.5 text-sm transition-colors text-muted hover:bg-soft hover:text-body data-active:bg-primary-500/10 data-active:text-primary-400',
    groupHeader: 'px-2 mb-2 text-xs font-bold uppercase tracking-widest text-muted/60',
    toggle: 'size-7 rounded-md hover:bg-soft',
  }}
/>

Render PropsLink

componentItem and componentGroup are structural replacements: when provided, the default piece is not rendered at all, giving you full control over the markup (for example custom navigation wrappers or route-specific icons).

Render PropProps Received
componentItem{ route, activePath, activeRoute, isActive, depth }
componentGroup{ group, isGroupActive, children }
tsx
<Sidebar.Items
  routes={routes}
  componentItem={({ route, isActive, depth }) => (
    <CustomLink active={isActive} depth={depth} route={route} />
  )}
  componentGroup={({ group, isGroupActive, children }) => (
    <nav aria-label={group.title}>
      <h2>{group.title}</h2>
      {children}
    </nav>
  )}
/>
Last updated on October 18, 2026

Was this page helpful?