Theme Primitives
Theme every primitive — Sidebar, OnThisPage, Navbar, Tabs — with your own CSS. Primitives are style-neutral: structure, behavior, and data-* state attributes come from the framework; colors and sizes belong to your theme.
Every primitive in Boltdocs is style-neutral. The framework owns structure, behavior, and state, while your theme owns every visual decision. This is what lets two completely different sites run on the same component code.
primitive → <aside data-sidebar-root> + data-active, data-open, data-depth …
structure + behavior + state (no colors, no sizes)
theme → Tailwind v4 classes or CSS targeting those data-* attributes
colors, borders, spacing, transitions — all yours
The Contract
A primitive renders semantic HTML, wires up behavior (scroll tracking, open/close, active detection), and exposes its state through data-* attributes that are present only when true. Your theme never needs to know how the state is computed — only that it exists.
// What the framework renders when a sidebar link is the active page:
<a href="/docs/guides" data-active data-depth="1" aria-current="page" …>
Guides
</a>
/* What your theme does with it: */
a[data-active] {
color: var(--color-primary-500);
background-color: color-mix(in oklab, var(--color-primary-500) 10%, transparent);
}
Prefer Tailwind v4 variants when available:
<Sidebar.Link className="data-active:bg-primary-500/10 data-active:text-primary-400" />
Which States Does Each Primitive Expose?
| Primitive | State Attributes | Behavior in the primitive |
|---|---|---|
Sidebar | data-active, data-open, data-collapsible, data-depth, data-badge + aria-current, aria-expanded | Collapse/expand groups, active-route detection, scroll-into-view of the active item |
OnThisPage | data-active, data-level, data-otp-* + aria-current | IntersectionObserver scroll tracking, active-track indicator math, auto-scroll of the active item |
Navbar | data-open (mobile drawer + more menu) + aria-expanded | Mobile drawer, "more" dropdown, click-outside handling |
Tabs | data-selected + aria-selected | Keyboard roving-tabindex, indicator position |
Because the state lives in attributes, you can also target parent–child combinations that CSS is great at:
/* Indent level-3 items (attribute value) */
li[data-level='3'] a { padding-left: 1rem; }
/* Color a group chevron only when the group is open */
a[data-open] svg { transform: rotate(90deg); }
Defaults Live in ui-base
The look you get out of the box — the DocsLayout sidebar, the right-rail TOC, the sticky navbar — is implemented on top of the primitives in packages/core/src/client/components/ui-base/. Every class there is just the framework's own theme and is entirely overridable: the primitives accept className slots (or itemClassName / linkClassName / indicatorClassName / fadeClassName where the slot is repeated per item) that are merged with, and win over, the defaults.
// ui-base/sidebar.tsx — the framework's default look (yours will differ)
<Sidebar.Link className={cn('w-full text-sm font-medium py-2 pl-3 pr-3 …', isActive && 'data-active:bg-primary-500/10 …')} />
// your theme — overrides it entirely
<Sidebar.Link className="text-[13px] data-active:bg-accent data-active:text-accent-foreground" />
Building Your Own Theme File
The cleanest pattern is a docs/src/theme/ module that re-exports styled versions of the primitives, then passing those to DocsLayout (or composing them yourself):
// docs/src/theme/on-this-page.tsx
import { OnThisPage as Primitive } from 'boltdocs/primitives'
export function OnThisPage() {
return (
<Primitive.Root className="sticky top-navbar hidden xl:flex flex-col shrink-0 w-toc py-4 pl-6 pr-4">
<Primitive.Header className="text-xs font-bold uppercase tracking-wider text-muted mb-4">
On this page
</Primitive.Header>
<Primitive.Tree
headings={currentRoute.headings}
className="space-y-0.5 text-sm border-l border-subtle"
itemClassName="data-[level=3]:pl-3"
linkClassName="block py-0.5 pl-4 text-[13px] transition-colors text-muted hover:text-body data-active:text-primary-500"
indicatorClassName="-left-px w-0.5 rounded-full bg-primary-500"
fadeClassName="bg-linear-to-b from-transparent to-main"
/>
</Primitive.Root>
)
}
The w-* utility classes above are theme tokens, not framework API: w-toc, w-sidebar, top-navbar, text-muted, border-subtle, bg-main, and the data-active:* color variants are all defined by the site theme (in Tailwind v4, via @theme tokens and custom variants) — your site defines its own.
Styling With Plain CSS
If you are not using Tailwind, the same contract works with a stylesheet:
.sidebar-link { padding: 0.5rem 0.75rem; font-size: 0.875rem; }
.sidebar-link[data-active] {
color: #6366f1;
background: rgba(99, 102, 241, 0.1);
font-weight: 600;
}
.sidebar-group[data-open] > button { color: currentColor; }
No framework-generated class names to reverse-engineer — the data-* attributes are a stable, documented contract.