Navbar

Sticky top bar chrome

Intent

Navbar is the sticky top bar of a site or app shell: logo on one side, nav in the middle, actions on the other. It owns the chrome -- surface, border, padding, stickiness, z-index -- and nothing else. Interior layout comes from composition (.cluster, .center) and link/button atoms (nav-link, button).

An optional .subnavbar sticks just below the navbar -- a breadcrumb trail, a section tab strip, a filter bar. It offsets by --navbar-height so the two bars stack cleanly when both are visible.

Semantic structure

.navbar is chrome -- the sticky strip, the surface, the border. The semantic markup that wraps it is the consumer's responsibility, and getting it right matters for screen-reader landmark navigation, keyboard users, and the document outline. The recommended scaffold:

<header class="navbar">
  <div class="cluster is-between">
    <a href="/"><strong>Brand</strong></a>

    <nav class="cluster" aria-label="Primary">
      <a class="nav-link" aria-current="page">Home</a>
      <a class="nav-link">Products</a>
    </nav>

    <div class="cluster">
      <button class="button">Sign in</button>
    </div>
  </div>
</header>

Three landmark / role decisions live in that scaffold:

  • <header> is the wrapper. It exposes the banner landmark when it's a top-level child of <body> -- screen- reader users can jump to it via landmark navigation. Don't add a role attribute; the implicit landmark already works.
  • Inner <nav> with aria-label. The actual navigation links go inside a <nav> element, not directly on the <header>. The navigation landmark is for the nav region; the banner landmark is for the bar as a whole. aria-label distinguishes multiple <nav> elements on the page (primary nav, footer nav, sidebar nav, breadcrumb) -- screen readers announce each by its label, so they need to differ.
  • Brand and actions stay outside the inner <nav>. A brand link is a logo, not navigation in the screen-reader sense; action buttons (sign in, account menu, search) are controls, not navigation. Keeping them outside the <nav> keeps the navigation landmark focused on real nav links, which is what users expect when they jump to it.

.subnavbar is generic chrome -- the right semantic element depends on what's inside it. Pick to match:

  • Section nav / tab strip: <nav class="subnavbar" aria-label="Section">. A second navigation landmark, distinguished from the primary nav by its label.
  • Breadcrumb: <nav class="subnavbar" aria-label="Breadcrumb"> with an <ol> inside. The ordered list signals the hierarchy; the label tells the screen reader it's a breadcrumb.
  • Filter / sort / view controls (toolbar): <div class="subnavbar" role="toolbar">. The toolbar role tells screen readers to navigate the bar with arrow keys, matching the pattern for command strips in native apps. A dedicated .toolbar component with full keyboard semantics is on the roadmap; for now, the role + .subnavbar chrome is the right shape.

Don't repeat the primary nav inside the subnavbar -- duplicate landmarks make screen-reader navigation harder, not easier. If the secondary content has no obvious semantic wrapper, a plain <div class="subnavbar"> is fine.

Basic usage

.navbar on a <header> wraps whatever composition matches the shell. The canonical shape uses .cluster.is-between to push three sections (logo, nav, actions) apart; the <nav> landmark surrounds only the actual navigation links.

<docs-resize>
    <header class="navbar">
        <div class="cluster is-between">
            <a href="#"><strong>Acme</strong></a>
            <nav class="cluster" aria-label="Primary">
                <a href="#" class="nav-link" aria-current="page">Docs</a>
                <a href="#" class="nav-link">Components</a>
                <a href="#" class="nav-link">Blog</a>
            </nav>
            <div class="cluster">
                <button class="button is-ghost" type="button">Sign in</button>
                <button class="button" type="button">Sign up</button>
            </div>
        </div>
    </header>
</docs-resize>

.subnavbar is a second sticky bar that offsets below the navbar. It reads --navbar-height (falling back to 3rem when unset) so it knows where to stick. Set the token on the navbar instance (or on :root) to match the actual rendered height -- the default covers a single-line navbar with snug padding, but a taller navbar (larger logo, extra vertical padding) needs the token set explicitly or the subnavbar sticks too high and overlaps.

<docs-resize>
    <header class="navbar">
        <div class="cluster is-between">
            <a href="#"><strong>Acme</strong></a>
            <nav class="cluster" aria-label="Primary">
                <a href="#" class="nav-link" aria-current="page">Docs</a>
                <a href="#" class="nav-link">Components</a>
            </nav>
        </div>
    </header>
    <nav class="subnavbar" aria-label="Section">
        <div class="breadcrumb">
            <ol>
                <li><a href="#">Home</a></li>
                <li><a href="#">Products</a></li>
                <li><span aria-current="page">Widget</span></li>
            </ol>
        </div>
    </nav>
</docs-resize>

Without the bottom border

.navbar.no-border drops the bottom border. Use when the navbar sits on a surface that already provides visual separation or when the design calls for a borderless top bar. .subnavbar.no-border is the same exception for the secondary bar.

<docs-resize>
    <header class="navbar no-border">
        <div class="cluster is-between">
            <a href="#"><strong>Acme</strong></a>
            <div class="cluster">
                <button class="button is-ghost" type="button">Help</button>
                <button class="button" type="button">Account</button>
            </div>
        </div>
    </header>
</docs-resize>

Class reference

Class Role
.navbar Sticky top bar. Surface + bottom border + padding. z-index set from the sticky token so it layers above page content.
.subnavbar Secondary sticky bar. Offsets below the navbar via --navbar-height; z-index sits one below the navbar so the two don't fight.
.navbar.no-border, .subnavbar.no-border Exception: drops the bottom border for designs that don't want the separator.

Customization

.navbar and .subnavbar each expose a scoped background / border / padding trio (--navbar-bg, --navbar-border, --navbar-padding and the three --subnavbar-* counterparts). Override on a single instance for a one-off tone or on :root for site-wide retoning.

--navbar-height is the load-bearing coupling: the subnavbar consumes it as its sticky offset. Unset, it falls back to 3rem, which matches a single-line navbar with snug padding. If you change the navbar's padding, font size, or content height, set --navbar-height to match the rendered height or the subnavbar will stick at the wrong position.

Full token list and defaults: src/css/navbar.css.

Accessibility

  • <header> is a landmark. The element exposes itself as banner when it's a top-level page child, letting screen-reader users jump to it via landmark navigation. Put nav links inside an inner <nav> with aria-label rather than on the <header> itself -- the banner landmark is for the bar as a whole; the nav landmark is for just the navigation region.
  • Sticky doesn't mean fixed. The navbar scrolls with the page until it reaches the top, then sticks. Users can scroll past it in a short viewport (e.g., a sidebar drawer on mobile) -- nothing special to wire, but be aware the top of the document may not always be "below the navbar" visually.
  • Skip link. A sticky navbar benefits from a skip-to-main-content link as the first focusable element -- keyboard and screen-reader users can bypass the repeated nav on every page. Hide it until focused with a .visually-hidden-style utility.
  • macOS keyboard quirk: Tab on macOS skips <a> links by default -- the OS only includes form controls in Tab order unless the user has enabled full keyboard navigation in their accessibility settings. Windows and Linux include links natively. .nav-links in a navbar are focusable in every browser; the difference is whether Tab visits them. Expected platform behavior, not a brio limitation.

For a multi-component example mixing nav-link siblings, a Products dropdown, a Resources megamenu popover, a search form, and an icon-only notifications trigger inside a navbar, see the In a navbar section of dropdown's docs. Useful as a cross-browser reference for common navbar patterns and as a test case for component coexistence.

CSS Reference: Navbar

Sticky top bar

Source: src/css/navbar.css

TokenDefaultDescription
--navbar-bgvar(--color-surface)Background color for the navbar
--navbar-bordervar(--border-width-default) solid var(--color-border-subtle)Bottom border for the navbar
--navbar-paddingvar(--ui-pad-y-snug) var(--ui-pad-x-snug)Inner padding for the navbar
ClassDescription
.navbarcomponent root
.no-borderRemoves the bottom border

CSS Reference: Subnavbar

Sticky secondary bar below the navbar

Source: src/css/navbar.css

TokenDefaultDescription
--subnavbar-bgvar(--color-surface)Background color for the subnavbar
--subnavbar-bordervar(--border-width-default) solid var(--color-border-subtle)Bottom border for the subnavbar
--subnavbar-paddingvar(--ui-pad-y-compact) var(--ui-pad-x-snug)Inner padding for the subnavbar
--navbar-height4remSticky offset from the top, set on navbar or :root
ClassDescription
.subnavbarcomponent root
.no-borderRemoves the bottom border