Navbar
The top header bar — branding, links, search trigger, theme toggle, social icons, and mobile drawer.
Navbar
Navbar is the sticky header at the top of every Boltdocs page. It hosts branding (logo + title), primary navigation links, an optional search trigger, theme toggle, social icons, and a mobile drawer.
Two flavors ship:
- Default —
import { Navbar } from 'boltdocs'(or'boltdocs/client'). It's styled and reads config fromboltdocs.config.ts. - Primitive —
import { Navbar } from 'boltdocs/primitives'. Same composition API, fully unstyled. This page is about the primitive.
Import
import { Navbar } from 'boltdocs/primitives'
Anatomy
The Navbar primitive exposes a small set of sub-components you string together:
| Sub-component | Purpose |
|---|---|
Navbar (root) | The <header> container. Sticky, full-width, top z-50. |
Navbar.Content | Centered inner flex row. Caps at max-w-3xl. |
Navbar.Left / Navbar.Center / Navbar.Right | Three flex zones. Center is hidden on mobile; left+right always show. |
Navbar.Logo | Wraps src + alt + optional width/height inside a router link. |
Navbar.Title | Plain-text branding label. |
Navbar.Links | Inline nav row, hidden on mobile. |
Navbar.Link | A single nav link. Optional external icon. |
Navbar.Dropdown | Hover-open popover with sub-items. |
Navbar.DropdownItem | A single sub-item inside a Navbar.Dropdown. |
Navbar.SearchTrigger | Button that opens the search dialog. Auto-renders desktop & mobile variants. |
Navbar.SearchTrigger.Kbd | The ⌘K hint alongside the desktop search trigger. |
Navbar.Theme | Sun/Moon toggle button. Stateless — pair with useTheme(). |
Navbar.Socials | Single-icon social link (github, discord, x, bluesky). |
Navbar.Split | Thin vertical divider between zones. |
Navbar.More | "⋯" button (mobile only) that opens the drawer. |
Navbar.MobileMenu | Full-screen modal drawer for mobile. |
Navbar.MobileLink | Big touch-friendly link for inside the drawer. |
The simplest Navbar
import { Navbar } from 'boltdocs/primitives'
export function MyNavbar() {
return (
<Navbar className="bg-white/80 dark:bg-black/80 backdrop-blur-md border-b border-subtle">
<Navbar.Content>
<Navbar.Left>
<Navbar.Logo src="/logo.svg" alt="My Docs" />
<Navbar.Title>My Docs</Navbar.Title>
</Navbar.Left>
<Navbar.Right>
<a href="/docs" className="text-sm font-medium">Docs</a>
<a href="/blog" className="text-sm font-medium">Blog</a>
</Navbar.Right>
</Navbar.Content>
</Navbar>
)
}
That's the entire shape — three zones, plenty of room to put whatever you want inside.
Production-ready Navbar
A fuller example with search, theme toggle, social links, and a mobile drawer. It uses Boltdocs' hooks to stay in sync with boltdocs.config.ts.
A note on React Hooks — always call hooks at the top level of a component, never inside
.map()callbacks or loops. The example below pre-computes localized hrefs before the JSX so the rules of hooks aren't violated.
// src/components/ProductionNavbar.tsx
import { useState, useMemo } from 'react'
import { Navbar, Menu, Button } from 'boltdocs/primitives'
import {
useNavbar,
useTheme,
useSearch,
useUI,
useVersion,
useI18n,
useLocation,
useLocalizedTo,
} from 'boltdocs/client'
import { Menu as MenuIcon, X, ChevronDown, Languages, Github } from 'lucide-react'
export function ProductionNavbar() {
// ── Hooks — always at the top of the component body ─────────────────────
const { links, title, logo, logoProps, github } = useNavbar()
const { theme, toggleTheme } = useTheme()
const { openSearch } = useSearch()
const { isSidebarOpen, toggleSidebar } = useUI()
const { pathname } = useLocation()
const { currentVersionLabel, availableVersions, handleVersionChange } = useVersion()
const { currentLocale, availableLocales, handleLocaleChange } = useI18n()
const localize = useLocalizedTo
// Pre-compute localized hrefs ONCE per render, indexed by link href.
const toPath = useMemo(
() => Object.fromEntries(
links.map((l) => [l.href, localize(l.href ?? '/')]),
),
[links, localize],
)
const isActive = (path: string) =>
pathname === path || pathname.startsWith(`${path}/`)
const [mobileOpen, setMobileOpen] = useState(false)
// ── Reusable inline dropdowns ──────────────────────────────────────────
const VersionDropdown = availableVersions.length > 0 && (
<Menu.Trigger>
<Button className="flex items-center gap-1 px-3 py-1.5 border rounded-lg text-xs bg-surface font-semibold hover:bg-zinc-50 dark:hover:bg-zinc-900">
{currentVersionLabel}
<ChevronDown size={14} className="opacity-60" />
</Button>
<Menu.Root className="w-44 mt-2 bg-white dark:bg-zinc-900 border border-subtle rounded-lg p-1 shadow-lg">
<Menu.Section items={availableVersions}>
{(v) => (
<Menu.Item
key={v.value}
onPress={() => handleVersionChange(v.value)}
className="px-3 py-2 text-xs rounded outline-none hover:bg-zinc-100 dark:hover:bg-zinc-800"
>
{v.label}
</Menu.Item>
)}
</Menu.Section>
</Menu.Root>
</Menu.Trigger>
)
const LocaleDropdown = availableLocales.length > 0 && (
<Menu.Trigger>
<Button className="flex items-center gap-1 px-3 py-1.5 border bg-surface rounded-lg text-xs font-semibold hover:bg-zinc-50 dark:hover:bg-zinc-900">
<Languages size={14} className="text-primary-500" />
<span className="uppercase">{currentLocale}</span>
<ChevronDown size={14} className="opacity-60" />
</Button>
<Menu.Root className="w-44 mt-2 bg-white dark:bg-zinc-900 border border-subtle rounded-lg p-1 shadow-lg">
<Menu.Section items={availableLocales}>
{(l) => (
<Menu.Item
key={l.value}
onPress={() => handleLocaleChange(l.value)}
className="px-3 py-2 text-xs rounded outline-none hover:bg-zinc-100 dark:hover:bg-zinc-800"
>
{l.label}
</Menu.Item>
)}
</Menu.Section>
</Menu.Root>
</Menu.Trigger>
)
// ── JSX ────────────────────────────────────────────────────────────────
return (
<>
<Navbar className="bg-main/80 backdrop-blur-md border-b border-subtle">
<Navbar.Content>
<Navbar.Left>
{/* Sidebar opener on mobile */}
{pathname.startsWith('/docs') && (
<Button
onPress={toggleSidebar}
className="lg:hidden p-1.5 mr-1 text-muted hover:text-body"
aria-label="Toggle sidebar"
>
{isSidebarOpen ? <X size={20} /> : <MenuIcon size={20} />}
</Button>
)}
{logo && <Navbar.Logo src={logo} alt={logoProps?.alt || title} href="/" />}
<Navbar.Title href="/">{title}</Navbar.Title>
<div className="hidden sm:block">{VersionDropdown}</div>
</Navbar.Left>
<Navbar.Center>
<Navbar.SearchTrigger onPress={openSearch}>
<span className="text-muted text-sm truncate">
Search the docs…
</span>
<Navbar.SearchTrigger.Kbd />
</Navbar.SearchTrigger>
</Navbar.Center>
<Navbar.Right>
<Navbar.Links>
{links.map((link) => {
const href = toPath[link.href ?? '/']
const active = isActive(href)
const items = link.items
if (items?.length) {
return (
<Navbar.Dropdown
key={link.href}
label={
<span className={active ? 'text-primary font-semibold' : 'text-muted hover:text-body'}>
{link.label}
</span>
}
>
{items.map((sub) => (
<Navbar.DropdownItem
key={sub.href}
href={toPath[sub.href ?? '/']}
label={sub.label}
/>
))}
</Navbar.Dropdown>
)
}
return (
<Navbar.Link
key={link.href}
href={href}
label={link.label}
active={active}
className={active ? 'text-primary font-semibold' : 'text-muted hover:text-body'}
/>
)
})}
</Navbar.Links>
<div className="hidden sm:flex items-center gap-2">
{LocaleDropdown}
<Navbar.Split className="bg-subtle" />
</div>
<Navbar.Theme theme={theme} onThemeChange={toggleTheme} />
{github && (
<Navbar.Socials icon="github" link={github} />
)}
<Navbar.More onPress={() => setMobileOpen(true)} className="lg:hidden" />
</Navbar.Right>
</Navbar.Content>
</Navbar>
{/* Mobile drawer */}
<Navbar.MobileMenu isOpen={mobileOpen} onClose={() => setMobileOpen(false)}>
<div className="flex flex-col gap-1">
{links.map((link) => (
<Navbar.MobileLink
key={link.href}
href={toPath[link.href ?? '/']}
label={link.label}
onPress={() => setMobileOpen(false)}
className="text-lg py-2"
/>
))}
</div>
<div className="mt-6 pt-6 border-t border-subtle flex flex-col gap-4">
{availableVersions.length > 0 && (
<div className="flex items-center justify-between px-4">
<span className="text-xs font-semibold uppercase text-muted">Version</span>
{VersionDropdown}
</div>
)}
{availableLocales.length > 0 && (
<div className="flex items-center justify-between px-4">
<span className="text-xs font-semibold uppercase text-muted">Language</span>
{LocaleDropdown}
</div>
)}
</div>
</Navbar.MobileMenu>
</>
)
}
Sub-components in depth
Navbar.Logo — branding
<Navbar.Logo
src="/logo-light.svg"
alt="MyDocs"
width={28}
height={28}
href="/"
/>
The logo is wrapped in a router-aware <Link>, so clicking the logo navigates home without a full page load. If you pass a dark-mode logo, supply it via boltdocs.config.ts instead — the Logo primitive only handles the light variant.
Navbar.Title — text branding
<Navbar.Title>MyDocs</Navbar.Title>
Hidden on small screens (< sm); pair it with a logo if you want branding at every breakpoint.
Navbar.Link & Navbar.Links
Links is just a flex row. Link is a single anchor wrapped in a router Link. Pass active to flip the styling when the path matches.
<Navbar.Links>
<Navbar.Link href="/docs" label="Docs" active={true} />
<Navbar.Link href="/blog" label="Blog" />
<Navbar.Link href="https://github.com/me" label="GitHub" to="external" />
</Navbar.Links>
to="external" renders a small outbound arrow after the label and adds target="_blank".
Navbar.Dropdown & Navbar.DropdownItem
Hover-open popovers for nested nav items. The Dropdown accepts any ReactNode as label — pass a <span> and style it however you like.
<Navbar.Dropdown label={<span>Products</span>}>
<Navbar.DropdownItem href="/products/boltdocs" label="Boltdocs" />
<Navbar.DropdownItem href="/products/nevermined" label="Nevermined" />
</Navbar.Dropdown>
Navbar.SearchTrigger
A pre-styled button that opens the search dialog. Pass onPress (which you wire up to useSearch().openSearch) and an arbitrary label. The Kbd hint (⌘K / Ctrl+K) is rendered automatically via the nested Navbar.SearchTrigger.Kbd.
<Navbar.SearchTrigger onPress={openSearch}>
Search…
<Navbar.SearchTrigger.Kbd />
</Navbar.SearchTrigger>
Navbar.Theme
<Navbar.Theme theme={theme} onChange={toggleTheme} />
theme is a literal 'dark' | 'light'. Boltdocs' useTheme() hook returns exactly that signature, so it's a drop-in.
Navbar.Socials
A pre-styled icon link. The supported icons are 'github' | 'discord' | 'x' | 'bluesky':
<Navbar.Socials icon="github" link="https://github.com/me" />
<Navbar.Socials icon="discord" link="https://discord.gg/..." />
Navbar.Split
A thin vertical line. Use it between sub-zones inside Navbar.Right to separate utility widgets from primary nav:
<Navbar.Right>
<Navbar.Links>...</Navbar.Links>
<Navbar.Split className="bg-subtle" />
<Navbar.Theme theme={theme} onChange={toggleTheme} />
</Navbar.Right>
Navbar.MobileMenu & Navbar.MobileLink
MobileMenu is a fullscreen modal drawer. It's mounted globally with position: fixed; toggle visibility via isOpen / onClose. Inside the menu, use MobileLink for big tap targets:
const [open, setOpen] = useState(false)
<>
<Navbar.More onPress={() => setOpen(true)} />
<Navbar.MobileMenu isOpen={open} onClose={() => setOpen(false)}>
<Navbar.MobileLink href="/" label="Home" onPress={() => setOpen(false)} />
<Navbar.MobileLink href="/docs" label="Docs" onPress={() => setOpen(false)} />
</Navbar.MobileMenu>
</>
The menu handles
env(safe-area-inset-*)automatically so it works on iOS notched devices without extra CSS.
Common pitfalls
- Calling hooks inside
.map()— pre-compute values withuseMemoor a plainforloop, then read from the result inside the JSX. React's rules of hooks require hooks at the top level only. - Forgetting
onPresson the search trigger — without it the button does nothing on click. Always wire it touseSearch().openSearch. - Wrapping
MobileMenuconditionally — putting<Navbar.MobileMenu />insideif (open)will unmount the dialog state when you close it. Keep it mounted and toggle viaisOpeninstead.
See also
Sidebar— the directory navigation that complements the navbar.useSearch— pairs withNavbar.SearchTrigger.useTheme— drivesNavbar.Themeand the global CSS variables.- Theming guide — how to vary navbar styling per route, locale, or version.