Vanilla Breeze

nav-bar

Top-level site/app navigation primitive. HTML-first wrap or JSON-first .items setter; auto aria-current sync via pathname matching with popstate listener.

Overview

The <nav-bar> component is the top-level navigation primitive for VB sites and apps. It does one thing: synchronizes aria-current="page" on the matching link as the URL changes, so consumers stop hand-rolling that logic. There are two ways to use it:

  • HTML-first: wrap your existing <a> children — pathname matching and popstate sync run automatically.
  • JS-first: assign .items = [...] and let the default renderer build the link list from data.

It is deliberately small — no router, no click interception, no custom renderer in v1. See Reactive integration for the trade-offs.

HTML-first

Wrap a normal set of links. Without JS the nav still works as plain HTML; with JS, aria-current="page" appears on the link that best matches location.pathname.

Matching policy:

  1. Exact pathname match wins.
  2. Otherwise, longest href prefix wins. So on /docs/elements/web-components/nav-bar/ the link /docs/elements/ beats /docs/.
  3. Ties broken by first-in-DOM-order.

Add data-match="exact" on the <nav-bar> to opt out of prefix matching.

Nested links (e.g. inside <drop-down> or <li>)

By default nav-bar only manages direct <a> children. If your link list is wrapped (each item inside a <li>, or each section in a <drop-down>), tag the canonical link in each item with data-nav-link:

When any descendant link carries data-nav-link, those become the candidate set; otherwise direct-children behavior applies.

JS-first

When your routes live in JSON or are produced by code, assign .items. The component renders <a> children with the default markup.

The setter is idempotent — assigning a deep-equal array is a no-op and emits no event.

Item shape

FieldTypeDescription
hrefstringRequired. Anchor target.
labelstringRequired. Link text.
routestringOptional. .current diff target. Falls back to href.
iconstringOptional. Renders <icon-wc name="..."> before the label.
externalbooleanAdds target="_blank", rel="noopener noreferrer", and an SR-only "(opens in new tab)" suffix.
badgestring | numberOptional pill chip after the label.

Properties

PropertyTypeDescription
.itemsNavItem[] | nullRead returns last assignment, else inferred from children. Set replaces children with default renderer; idempotent.
.currentstring | nullRead returns the active route (data-route or href of the aria-current link). Set takes ownership and disables popstate sync.

Methods

MethodDescription
.releaseCurrent()Restore auto mode after explicit assignment to .current. Re-runs pathname matching.
.refresh()Manually re-run pathname matching. Useful for AJAX frameworks (e.g. HTML-Star) that change the URL without firing popstate. No-op when .current is explicitly owned.

Events

EventDetailWhen
nav-bar:upgradedOnce after first connect.
nav-bar:items-changed{ items, source: 'property' }After .items is assigned to a new value.
nav-bar:current-changed{ current, previous, source }source is one of 'property' | 'popstate' | 'pathname'.

Reactive integration (important caveat)

v1 does not support driving .items from a signal/effect with preserved in-flight state. Each .items assignment fully replaces children. There is no keyed diff, so a link mid-focus loses focus on reassignment, and any <icon-wc> inside re-mounts. This is fine for nav (links don't carry load-bearing state) but it is not the right primitive for reactive route lists.

If you need reactive routing with preserved state, use your framework's link primitive instead of <nav-bar>:

  • Montane — use Link() from montane/router.js. It is bound to the Montane router and reactively tracks active state. <nav-bar> would duplicate that work less well.
  • View-switching without a router — use <content-swap>.
  • Other frameworks — keep using their Link primitive. <nav-bar> is a drop-in for HTML-first sites and JSON-driven apps without a reactive router.

If your use case needs renderItem or keyed diff, open an issue with the concrete scenario so we can scope a Phase 2.

Composition

  • Inside <mobile-menu>nav-bar is content; mobile-menu is the off-canvas shell. They compose: <mobile-menu><nav-bar>...</nav-bar></mobile-menu>.
  • With <drop-down> — wrap an item in <drop-down> for a sub-menu. nav-bar only manages its direct <a> children.
  • Not a router and not a view-switcher. See Reactive integration.

Accessibility

  • Sets role="navigation" on the host if absent.
  • aria-current="page" on the active link only.
  • External links get target="_blank", rel="noopener noreferrer", and an SR-only "(opens in new tab)" suffix.
  • Native keyboard navigation — Tab through links, Enter / Space activates. No custom focus management.