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 Example
Build a custom responsive sidebar using the primitive containers and the automatic routing hook:
// 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 Reference
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:
// 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-*)
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.
| Attribute | Emitted On | Present When |
|---|---|---|
data-active | Sidebar.Link anchor, Sidebar.Group container | The link / group contains the current route |
data-open | Collapsible group button + chevron, Sidebar.SubGroup toggle + chevron | The section is expanded |
data-collapsible | Sidebar.Group container | The group renders a toggle button |
data-depth | Sidebar.Link anchor | Nesting depth (number) |
data-badge | Badge span | Badge text / type (e.g. new, updated, deprecated) |
aria-current | Sidebar.Link anchor | "page" when active |
aria-expanded | Toggle buttons | "true" / "false" |
Styling the active item from your theme — Tailwind v4:
<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:
[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 Subcomponents
The Sidebar primitive provides the following subcomponents for structure customization:
| Component | HTML Tag | Description | Props |
|---|---|---|---|
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.Item | Varies | Recursive router mapping utility rendering links or subgroups automatically. | Sidebar.Item Props |
Sidebar.Items | Varies | Automated loop container mapping and rendering a collection of routes. | Sidebar.Items Props |
Component Props
ComponentBaseProps (Common)
| Property | Type | Default | Description |
|---|---|---|---|
children | ReactNode | undefined | Child content elements. |
className | string | undefined | Custom utility CSS classes. |
style | CSSProperties | undefined | Inline style settings. |
Sidebar.Mobile Props
| Property | Type | Default | Description |
|---|---|---|---|
overlayClassName | string | undefined | Classes for the full-screen overlay behind the drawer. |
Sidebar.Group Props
| Property | Type | Default | Description |
|---|---|---|---|
title | string | undefined | Group category title text. |
icon | ElementType | undefined | Custom leading icon component. |
children | ReactNode | Required | Sidebar child elements. |
collapsible | boolean | false | Renders the header as a toggle button instead of a heading. |
collapsed | boolean | false | Initial collapsed state for collapsible groups. |
active | boolean | false | Keeps the group open when it contains the active route. |
headerClassName | string | undefined | Extra classes for the group header (button or heading). |
iconClassName | string | undefined | Extra classes for the leading group icon. |
titleClassName | string | undefined | Extra classes for the title wrapper. |
contentClassName | string | undefined | Extra classes for the children wrapper. |
trailing | ReactNode | undefined | Custom content rendered at the end of the header. |
renderTitle | ({ title, isOpen }) => ReactNode | undefined | Full replacement for the header content. |
Sidebar.Link Props
| Property | Type | Default | Description |
|---|---|---|---|
label | string | Required | Display name label of the link. |
href | string | Required | Link route target path. |
active | boolean | false | Active state — reflected via data-active + aria-current="page". |
icon | ElementType | undefined | Leading icon element wrapper. |
badge | string | BadgeConfig | undefined | Notification status label. |
children | ReactNode | undefined | When provided, overrides the label content. |
iconClassName | string | undefined | Extra classes for the leading icon. |
labelClassName | string | undefined | Extra classes for the label text. |
trailing | ReactNode | undefined | Custom content rendered after the label. |
depth | number | undefined | Nesting depth — reflected via data-depth. |
Sidebar.SubGroup Props
Inherits all Sidebar.Link props plus:
| Property | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | false | Collapsed state visibility indicator. |
onToggle | () => void | undefined | Toggle trigger callback handler. |
children | ReactNode | Required | Subgroup navigation links. |
rootClassName | string | undefined | Extra classes for the subgroup wrapper. |
linkClassName | string | undefined | Extra classes for the subgroup link. |
contentClassName | string | undefined | Extra classes for the nested children wrapper. |
toggleClassName | string | undefined | Extra classes for the expand/collapse toggle. |
renderToggle | ({ isOpen }) => ReactNode | undefined | Full replacement for the toggle button. |
Sidebar.Item Props
| Property | Type | Default | Description |
|---|---|---|---|
route | ComponentRoute | Required | Route configuration object to parse. |
activePath | string | Required | Active window URL path. |
activeRoute | ComponentRoute | undefined | The resolved active route (enables file-path matching). |
depth | number | 0 | Current nesting depth, used by renderItem. |
renderItem | (route, depth) => ReactNode | undefined | Recursive render override for child routes. |
classNames | SidebarSlots | undefined | Per-piece class overrides merged over the defaults. |
Sidebar.Items Props
| Property | Type | Default | Description |
|---|---|---|---|
routes | ComponentRoute[] | Required | Array of route configurations to render recursively. |
classNames | SidebarSlots | undefined | Per-piece class overrides merged over the defaults. |
componentItem | (props: SidebarItemRenderProps) => ReactNode | undefined | Full render replacement for a route node (runs recursively for nested routes). |
componentGroup | (props: SidebarGroupRenderProps) => ReactNode | undefined | Full render replacement for a top-level group wrapper. |
Slots (classNames)
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:
| Key | Applied To | Default |
|---|---|---|
item | Leaf route link root | group flex items-center gap-2.5 outline-none |
itemIcon | Leaf route leading icon | shrink-0 |
itemLabel | Leaf route label | truncate |
group | Group wrapper | undefined |
groupHeader | Group header (button or heading) | undefined |
groupIcon | Group leading icon | undefined |
groupTitle | Group title wrapper | flex items-center gap-2 |
groupContent | Group children wrapper | flex flex-col |
subgroup | Subgroup wrapper | flex flex-col |
subgroupLink | Subgroup link | flex-1 pr-8 |
subgroupContent | Subgroup children wrapper | flex flex-col |
toggle | Subgroup expand/collapse toggle | absolute right-1 |
<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 Props
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 Prop | Props Received |
|---|---|
componentItem | { route, activePath, activeRoute, isActive, depth } |
componentGroup | { group, isGroupActive, children } |
<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>
)}
/>