1. Home
  2. ChevronRightLayout
  3. ChevronRightSidebar

Sidebar

The left navigation directory — grouped, collapsible, with optional badges and icons.

SidebarLink

Sidebar is the vertical navigation panel on the left side of the docs page. It's a composition of a few small pieces: a Header slot for branding or search input, a scrollable Content area, and an automated Items renderer that walks the route graph for you.

Two flavors ship:

  • Defaultimport { Sidebar } from 'boltdocs'. Fully styled, ready to drop in.
  • Primitiveimport { Sidebar } from 'boltdocs/primitives'. Same composition API, fully unstyled.

This page is about the primitive. Use it when you want full control over the visual treatment, but still want automated route → link rendering.


ImportLink

import { Sidebar } from 'boltdocs/primitives'

AnatomyLink

Sub-componentPurpose
Sidebar (root)The desktop <aside>. Sticky, full-height, scoped to lg+ viewports.
Sidebar.MobileA modal drawer. Same contents as desktop, dismissed via backdrop or close button.
Sidebar.HeaderOptional strip at the top of the panel. Good place for a search input or logo.
Sidebar.ContentThe scrollable list area. Restores scroll position across SPA navigations.
Sidebar.GroupA category wrapper with optional title + icon + collapsible toggle.
Sidebar.LinkA single navigation anchor with optional icon and badge.
Sidebar.SubGroupA nested collapsible group. Useful for two-level hierarchies.
Sidebar.ItemRecursive renderer for a single ComponentRoute. Picks between Link or SubGroup automatically.
Sidebar.ItemsThe high-level "render everything" loop. Pass the routes array from useRoutes().

Minimal exampleLink

import { Sidebar } from 'boltdocs/primitives'
import { useRoutes } from 'boltdocs/client'

export function MySidebar() {
  const { routes } = useRoutes()

  return (
    <>
      <Sidebar className="border-r border-subtle">
        <Sidebar.Content>
          <Sidebar.Items routes={routes} />
        </Sidebar.Content>
      </Sidebar>

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

Two copies of the same content, one for desktop and one for the mobile drawer. Boltdocs mounts both — the show/hide logic lives entirely in Tailwind breakpoints (hidden lg:flex on the desktop one, lg:hidden on the mobile one).

Sidebar.Items walks the routes array, finds groups via frontmatter, and renders each one as a collapsible section. Active route highlighting works automatically.


Real-world SidebarLink

The version that ships with Boltdocs by default — branded header, mobile close button, and version/language pickers in the mobile drawer.

Rules of Hooks reminder — keep useLocalizedTo and any other hook at the top of the component. Don't call it inside .map() callbacks or conditionals.

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

export function ProductionSidebar() {
  const { routes } = useRoutes()
  const { logo, title, logoProps } = useNavbar()
  const { isSidebarOpen, 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"
    />
  )

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

  return (
    <>
      {/* ── Desktop ──────────────────────────────────────────── */}
      <Sidebar className="border-r border-subtle bg-main">
        <Sidebar.Content>
          <Sidebar.Items routes={routes} />
        </Sidebar.Content>
      </Sidebar>

      {/* ── Mobile drawer ────────────────────────────────────── */}
      <Sidebar.Mobile className="bg-main">
        <Sidebar.Header className="flex items-center justify-between border-b border-subtle px-4 py-3">
          <div className="flex items-center gap-3 min-w-0">
            {SidebarLogo}
            <span className="font-bold text-base 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 hover:bg-surface"
            aria-label="Close sidebar"
          >
            <X size={18} />
          </Button>
        </Sidebar.Header>

        <Sidebar.Content className="p-4">
          {hasPickers && (
            <div className="grid grid-cols-2 gap-2 mb-4">
              {availableVersions.length > 0 && (
                <select
                  value={currentVersionLabel}
                  onChange={(e) => handleVersionChange(e.target.value)}
                  className="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>
              )}
              {availableLocales.length > 0 && (
                <select
                  value={currentLocale}
                  onChange={(e) => handleLocaleChange(e.target.value)}
                  className="bg-surface border border-subtle rounded-lg px-3 py-2 text-xs font-semibold"
                >
                  {availableLocales.map((l) => (
                    <option key={l.key} value={l.value}>{l.label}</option>
                  ))}
                </select>
              )}
            </div>
          )}
          <Sidebar.Items routes={routes} />
        </Sidebar.Content>
      </Sidebar.Mobile>
    </>
  )
}

Manual composition (no Items)Link

When you don't want the automated loop — for instance, when you're embedding the sidebar inside an iframe or a CMS preview pane — you can compose the routes yourself:

import { Sidebar } from 'boltdocs/primitives'
import {
  House,
  Rocket,
  Wrench,
  GitBranch,
  Puzzle,
} from 'lucide-react'

export function ManualSidebar() {
  return (
    <Sidebar className="border-r border-subtle">
      <Sidebar.Header className="px-4 py-3 flex items-center justify-between">
        <span className="text-xs font-bold uppercase tracking-wider text-muted">
          Documentation
        </span>
      </Sidebar.Header>

      <Sidebar.Content>
        {/* Optional ungrouped section */}
        <Sidebar.Group title="Getting Started">
          <Sidebar.Link href="/" label="Home" icon={House} />
          <Sidebar.Link href="/docs/installation" label="Installation" icon={Rocket} />
          <Sidebar.Link href="/docs/configuration" label="Configuration" icon={Wrench} />
        </Sidebar.Group>

        {/* Section with badge — "Updated" badge shows up next to the link */}
        <Sidebar.Group title="Plugins">
          <Sidebar.Link
            href="/docs/plugins/ask-ai"
            label="Ask AI"
            icon={Puzzle}
            badge="updated"
          />
          <Sidebar.Link
            href="/docs/plugins/mermaid"
            label="Mermaid"
            icon={Puzzle}
            badge="new"
          />
        </Sidebar.Group>
      </Sidebar.Content>
    </Sidebar>
  )
}

Note the differences:

  • Sidebar.Link accepts icon, which renders the icon inline before the label.
  • badge is either a string 'new' | 'updated' | 'deprecated' or { text: ..., variant?: ... } for custom messages.
  • Sidebar.Group becomes collapsible automatically when you add collapsible and a collapsed initial state.

Sub-componentsLink

Sidebar.GroupLink

<Sidebar.Group
  title="Plugins"
  icon={Puzzle}
  collapsible
  collapsed={false}
>
  {/* links */}
</Sidebar.Group>
  • title — displayed above the children. Hidden if omitted.
  • icon — small Lucide icon component to render next to the title.
  • collapsible + collapsed — makes the group a chevron toggle. collapsed is the initial state; Boltdocs auto-expands the group if it contains the active route.
<Sidebar.Link
  href="/docs/plugins/ask-ai"
  label="Ask AI"
  icon={Puzzle}
  active={pathname === '/docs/plugins/ask-ai'}
  badge="updated"
/>

active is auto-detected when using Sidebar.Item / Sidebar.Items. For manual composition, set it from useLocation() if you want highlight styling.

Sidebar.SubGroupLink

A nested collapsible group. Renders a Link on the left with a chevron toggle on the right:

<Sidebar.SubGroup
  label="Plugins"
  href="/docs/plugins"
  isOpen={open}
  onToggle={() => setOpen(!open)}
  active={pathname.startsWith('/docs/plugins')}
>
  <Sidebar.Link ... />
  <Sidebar.Link ... />
</Sidebar.SubGroup>

You rarely need this when using Sidebar.Items — the recursive renderer picks Link or SubGroup automatically based on route.routes / route.subRoutes.

Sidebar.HeaderLink

Best home for a local search input or branding inside the sidebar. It's just a <div> with padding + a bottom border by default — pass any className to override.


How groups get detectedLink

Sidebar.Items reads the group and groupTitle fields from each RouteMeta. Routes with the same group end up under the same Sidebar.Group; routes without a group are rendered in an implicit "main" group above all others.

Each group can also carry:

  • sidebarPosition/groupPosition — sort order within the sidebar.
  • collapsible/collapsed — initial collapsed state.
  • icon — group-level icon (looked up from boltdocs.config.ts icons or something you set yourself via routes:[].icon).

If you set groupIcon on a route, it shows up as the icon for the parent group instead of the link icon.


Common pitfallsLink

  • Calling useLocalizedTo inside .map() — pre-compute values with useMemo or a one-pass for loop.
  • Forgetting Sidebar.Mobile — the mobile drawer is not auto-rendered; mount a copy yourself.
  • Custom scroll containersSidebar.Content uses a hard-coded custom-scrollbar class. Wrap your own with the same class for visual consistency.
  • Sticky positioning breaks inside flex parentsposition: sticky doesn't behave well when an ancestor has overflow: hidden. If you're nesting sidebars, check the chain.

See alsoLink

  • Navbar — top nav that pairs with Sidebar for the full directory.
  • useRoutes — returns the route array you pass to Sidebar.Items.
  • useUI — exposes isSidebarOpen and closeSidebar to coordinate with mobile.
Last updated on July 27, 2026

Was this page helpful?