Sidebar
The left navigation directory — grouped, collapsible, with optional badges and icons.
Sidebar
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:
- Default —
import { Sidebar } from 'boltdocs'. Fully styled, ready to drop in. - Primitive —
import { 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.
Import
import { Sidebar } from 'boltdocs/primitives'
Anatomy
| Sub-component | Purpose |
|---|---|
Sidebar (root) | The desktop <aside>. Sticky, full-height, scoped to lg+ viewports. |
Sidebar.Mobile | A modal drawer. Same contents as desktop, dismissed via backdrop or close button. |
Sidebar.Header | Optional strip at the top of the panel. Good place for a search input or logo. |
Sidebar.Content | The scrollable list area. Restores scroll position across SPA navigations. |
Sidebar.Group | A category wrapper with optional title + icon + collapsible toggle. |
Sidebar.Link | A single navigation anchor with optional icon and badge. |
Sidebar.SubGroup | A nested collapsible group. Useful for two-level hierarchies. |
Sidebar.Item | Recursive renderer for a single ComponentRoute. Picks between Link or SubGroup automatically. |
Sidebar.Items | The high-level "render everything" loop. Pass the routes array from useRoutes(). |
Minimal example
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 Sidebar
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
useLocalizedToand 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)
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.Linkacceptsicon, which renders the icon inline before the label.badgeis either a string'new' | 'updated' | 'deprecated'or{ text: ..., variant?: ... }for custom messages.Sidebar.Groupbecomes collapsible automatically when you addcollapsibleand acollapsedinitial state.
Sub-components
Sidebar.Group
<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.collapsedis the initial state; Boltdocs auto-expands the group if it contains the active route.
Sidebar.Link
<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.SubGroup
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.Header
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 detected
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 fromboltdocs.config.tsicons or something you set yourself viaroutes:[].icon).
If you set groupIcon on a route, it shows up as the icon for the parent group instead of the link icon.
Common pitfalls
- Calling
useLocalizedToinside.map()— pre-compute values withuseMemoor a one-passforloop. - Forgetting
Sidebar.Mobile— the mobile drawer is not auto-rendered; mount a copy yourself. - Custom scroll containers —
Sidebar.Contentuses a hard-codedcustom-scrollbarclass. Wrap your own with the same class for visual consistency. - Sticky positioning breaks inside flex parents —
position: stickydoesn't behave well when an ancestor hasoverflow: hidden. If you're nesting sidebars, check the chain.
See also
Navbar— top nav that pairs withSidebarfor the full directory.useRoutes— returns the route array you pass toSidebar.Items.useUI— exposesisSidebarOpenandcloseSidebarto coordinate with mobile.