1. Home
  2. ChevronRightLayout
  3. ChevronRightNavbar

Navbar

The top header bar — branding, links, search trigger, theme toggle, social icons, and mobile drawer.

NavbarLink

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:

  • Defaultimport { Navbar } from 'boltdocs' (or 'boltdocs/client'). It's styled and reads config from boltdocs.config.ts.
  • Primitiveimport { Navbar } from 'boltdocs/primitives'. Same composition API, fully unstyled. This page is about the primitive.

ImportLink

import { Navbar } from 'boltdocs/primitives'

AnatomyLink

The Navbar primitive exposes a small set of sub-components you string together:

Sub-componentPurpose
Navbar (root)The <header> container. Sticky, full-width, top z-50.
Navbar.ContentCentered inner flex row. Caps at max-w-3xl.
Navbar.Left / Navbar.Center / Navbar.RightThree flex zones. Center is hidden on mobile; left+right always show.
Navbar.LogoWraps src + alt + optional width/height inside a router link.
Navbar.TitlePlain-text branding label.
Navbar.LinksInline nav row, hidden on mobile.
Navbar.LinkA single nav link. Optional external icon.
Navbar.DropdownHover-open popover with sub-items.
Navbar.DropdownItemA single sub-item inside a Navbar.Dropdown.
Navbar.SearchTriggerButton that opens the search dialog. Auto-renders desktop & mobile variants.
Navbar.SearchTrigger.KbdThe ⌘K hint alongside the desktop search trigger.
Navbar.ThemeSun/Moon toggle button. Stateless — pair with useTheme().
Navbar.SocialsSingle-icon social link (github, discord, x, bluesky).
Navbar.SplitThin vertical divider between zones.
Navbar.More"⋯" button (mobile only) that opens the drawer.
Navbar.MobileMenuFull-screen modal drawer for mobile.
Navbar.MobileLinkBig touch-friendly link for inside the drawer.

The simplest NavbarLink

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 NavbarLink

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 depthLink

<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>MyDocs</Navbar.Title>

Hidden on small screens (< sm); pair it with a logo if you want branding at every breakpoint.

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".

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>

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 theme={theme} onChange={toggleTheme} />

theme is a literal 'dark' | 'light'. Boltdocs' useTheme() hook returns exactly that signature, so it's a drop-in.

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/..." />

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>

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 pitfallsLink

  • Calling hooks inside .map() — pre-compute values with useMemo or a plain for loop, then read from the result inside the JSX. React's rules of hooks require hooks at the top level only.
  • Forgetting onPress on the search trigger — without it the button does nothing on click. Always wire it to useSearch().openSearch.
  • Wrapping MobileMenu conditionally — putting <Navbar.MobileMenu /> inside if (open) will unmount the dialog state when you close it. Keep it mounted and toggle via isOpen instead.

See alsoLink

  • Sidebar — the directory navigation that complements the navbar.
  • useSearch — pairs with Navbar.SearchTrigger.
  • useTheme — drives Navbar.Theme and the global CSS variables.
  • Theming guide — how to vary navbar styling per route, locale, or version.
Last updated on July 27, 2026

Was this page helpful?