NS Design System
v3.0.0 · 257 tokens

55Navbar

The one piece of chrome on every page: brand, links, actions, one hairline below, --navbar-h (3.5rem) tall — chrome, and every row it takes is a row of content the reader does not get. The current page wears the 2px brand underline — the accent-line device, horizontal — because the page you are already on is not an action and must not look like a button. Sticky by default; the surface variants ride a hero, float, or go quiet.

Use it for

  • The single global bar at the top of every page
  • aria-current="page" on exactly one link
  • Pair it with the mobile sheet below lg — the bar is one component with two link surfaces

Not for

  • Secondary navigation inside a page — Tabs or Sidebar
  • A second bar under the first — if the page needs two rows of navigation, the information architecture is the problem
  • Hiding the bar on scroll-down: it saves 3.5rem and costs everyone the ability to predict where navigation is

Anatomy

Brand · link row · divider · search · theme · action, in that order (the signed-in and signed-out ends of the bar are on the Account menu page). Everything after the link row sits in .ns-topnav__actions, which takes the remaining space with margin-inline-start:auto — so adding an action never re-centres the links. Live: the whole bar on this page is operable.

markup
<nav class="ns-topnav" aria-label="Anatomy example">
  <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span><span class="ns-topnav__brand-tag">Learn</span></a>
  <ul class="ns-topnav__links">
    <li><a href="#" aria-current="page">Courses</a></li>
    <li><a href="#">Docs <span class="ns-topnav__flag">New</span></a></li>
  </ul>
  <div class="ns-topnav__actions">
    <button type="button" class="ns-navsearch">
      <i class="ph ph-magnifying-glass" aria-hidden="true"></i>
      <span class="ns-navsearch__text">Search courses…</span>
      <kbd class="ns-navsearch__kbd">⌘K</kbd>
    </button>
    <span class="ns-topnav__divider" aria-hidden="true"></span>
    <button type="button" class="ns-themeswitch" role="switch" aria-checked="false" aria-label="Dark mode" data-ns-theme-toggle>
      <span class="ns-themeswitch__mark" aria-hidden="true"></span>
    </button>
    <a class="ns-btn ns-btn--primary ns-btn--sm" href="#">Sign up</a>
  </div>
</nav>

Contained

.ns-topnav__inner caps the contents at the page container so the brand sits exactly above the page's first column. Without it the bar is edge-to-edge — right for an app, wrong for a 72rem marketing page. --wide switches the cap to the wide container.

markup
<nav class="ns-topnav" aria-label="Contained example">
  <div class="ns-topnav__inner">
    <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span></a>
    <ul class="ns-topnav__links">
      <li><a href="#" aria-current="page">Courses</a></li>
      <li><a href="#">Pricing</a></li>
    </ul>
    <div class="ns-topnav__actions"><a class="ns-btn ns-btn--primary ns-btn--sm" href="#">Start learning</a></div>
  </div>
</nav>

Signed in — app bar

Compact height, search first, account menu last. No sign-up button: the member already signed up, and leaving it there is how a product tells a paying customer it has not noticed them.

markup
<nav class="ns-topnav ns-topnav--compact" aria-label="App bar example">
  <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span></a>
  <ul class="ns-topnav__links">
    <li><a href="#" aria-current="page">My learning</a></li>
    <li><a href="#">Catalog</a></li>
  </ul>
  <div class="ns-topnav__actions">
    <button type="button" class="ns-navsearch ns-navsearch--icon" aria-label="Search"><i class="ph ph-magnifying-glass" aria-hidden="true"></i></button>
    <button type="button" class="ns-themetoggle-icon" role="switch" aria-checked="false" aria-label="Dark mode" data-ns-theme-toggle>
      <i class="ph ph-sun" aria-hidden="true"></i><i class="ph ph-moon" aria-hidden="true"></i>
    </button>
    <span class="ns-topnav__divider" aria-hidden="true"></span>
    <div class="ns-usermenu">
      <button type="button" class="ns-usermenu__trigger" data-ns-menu aria-expanded="false" aria-controls="app-account" aria-label="Account menu for Aarti Kulkarni">
        <span class="ns-avatar ns-avatar--sm" aria-hidden="true">AK</span><span class="ns-usermenu__name">Aarti K.</span>
      </button>
      <div class="ns-usermenu__panel" id="app-account">
        <div class="ns-usermenu__head">
          <span class="ns-avatar" aria-hidden="true">AK</span>
          <span class="ns-usermenu__identity"><span class="ns-usermenu__fullname">Aarti Kulkarni</span><span class="ns-usermenu__email">aarti@example.com</span></span>
          <span class="ns-usermenu__plan">Pro</span>
        </div>
        <hr class="ns-menu__sep">
        <a class="ns-menu__item" href="#"><i class="ph ph-user" aria-hidden="true"></i> My learning</a>
        <a class="ns-menu__item" href="#"><i class="ph ph-gear" aria-hidden="true"></i> Settings</a>
      </div>
    </div>
  </div>
</nav>

With icons

Icons in the link row are OFF by default and this is the case they exist for: an app bar, where the links are PLACES in a product rather than pages of a site and the shape helps someone find the row again. On a marketing bar, where every link is a page, a glyph on each one means nothing. The icon rides the link's colour, so current and hover need no extra rule.

markup
<nav class="ns-topnav ns-topnav--compact" aria-label="Icon links example">
  BRAND
  <ul class="ns-topnav__links">
    <li><a href="#" aria-current="page"><i class="ph ph-squares-four" aria-hidden="true"></i> Dashboard</a></li>
    <li><a href="#"><i class="ph ph-books" aria-hidden="true"></i> My courses</a></li>
    <li><a href="#"><i class="ph ph-seal-check" aria-hidden="true"></i> Certificates</a></li>
  </ul>
  <div class="ns-topnav__actions">
    <span class="ns-navstat"><i class="ph ph-lightning" aria-hidden="true"></i> <strong>12</strong> day streak</span>
    <a class="ns-navicon" href="#" aria-label="Notifications — 3 unread"><i class="ph ph-bell" aria-hidden="true"></i><span class="ns-navicon__badge" aria-hidden="true">3</span></a>
    <button type="button" class="ns-themeswitch" role="switch" aria-checked="false" aria-label="Dark mode" data-ns-theme-toggle>
      <span class="ns-themeswitch__mark" aria-hidden="true"></span>
    </button>
  </div>
</nav>

Search panel

The bar's search is a BUTTON shaped like a field, and this is what it opens: the shared search dialog (templates/search-modal.html), where results have room and ⌘K reaches it from anywhere. Live — click it or press ⌘K/Ctrl-K. Typing in the bar itself would mean teleporting the text into a dialog on the first keystroke, which is the version of this that feels broken.

Start typing to search.

markup
<button type="button" class="ns-navsearch" data-ns-search>
  <i class="ph ph-magnifying-glass" aria-hidden="true"></i>
  <span class="ns-navsearch__text">Search courses…</span>
  <kbd class="ns-navsearch__kbd">⌘K</kbd>
</button>
<button type="button" class="ns-navsearch ns-navsearch--icon" aria-label="Search" data-ns-search>
  <i class="ph ph-magnifying-glass" aria-hidden="true"></i>
</button>
<dialog class="ns-modal ns-modal--lg" id="doc-search" aria-label="Search">
  <div class="ns-modal__header">
    <div style="flex:1">
      <label class="ns-visually-hidden" for="doc-search-input">Search courses and documentation</label>
      <input id="doc-search-input" class="ns-input ns-input--has-icon" type="search" placeholder="Search courses, lessons, docs…" autocomplete="off">
    </div>
    <button type="button" class="ns-btn ns-btn--quiet ns-btn--icon ns-btn--sm" aria-label="Close search" data-ns-search-close><i class="ph ph-x" aria-hidden="true"></i></button>
  </div>
  <div class="ns-modal__body" role="status" aria-live="polite">
    <div class="ns-empty" style="border:0">
      <i class="ph ph-magnifying-glass ns-empty__icon" aria-hidden="true"></i>
      <p class="ns-empty__text">Start typing to search.</p>
    </div>
  </div>
</dialog>

GitHub star

For an open-source product this is the bar's one piece of social proof, and it is a link to the repo rather than a widget: the count is a number, rendered mono like every other number here, and it is divided by a hairline so the pill reads as one control. One per bar. Below lg the words drop and the mark stands alone. The mark itself is filled rather than restroked — a logo has to be itself.

markup
<a class="ns-navstar" href="#" aria-label="Star Namaste Salesforce on GitHub — 1.2k stars">
  <span class="ns-navstar__label"><i class="ph ph-github-logo" aria-hidden="true"></i> <span>Star</span></span>
  <span class="ns-navstar__count" aria-hidden="true">1.2k</span>
</a>
<a class="ns-navicon" href="#" aria-label="GitHub repository"><i class="ph ph-github-logo" aria-hidden="true"></i></a>

Blog bar

Centred links, a reading-progress line along the bottom edge, and tags instead of products — on a blog the useful navigation is by subject. The progress line is set here to 38% for the specimen; in product assets/js/nav.js drives it from scroll. Full page: open the blog bar demo ↗

markup
<nav class="ns-topnav ns-topnav--center" aria-label="Blog bar example">
  <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span><span class="ns-topnav__brand-tag">Blog</span></a>
  <ul class="ns-topnav__links">
    <li><a href="#" aria-current="page">Latest</a></li>
    <li><a href="#">Admin</a></li>
    <li><a href="#">Developer</a></li>
    <li><a href="#">Architect</a></li>
  </ul>
  <div class="ns-topnav__actions">
    <button type="button" class="ns-navsearch ns-navsearch--icon" aria-label="Search the blog"><i class="ph ph-magnifying-glass" aria-hidden="true"></i></button>
    <a class="ns-btn ns-btn--primary ns-btn--sm" href="#">Subscribe</a>
  </div>
  <div class="ns-topnav__progress" style="--p:38" aria-hidden="true"></div>
</nav>

On a hero

--dark is the navy bar; --transparent is the same anatomy with no background at all, for sitting on a hero image — it swaps to this navy once the page scrolls (data-scrolled, one passive listener). Deliberately NOT a translucent blur: backdrop-filter costs real repaint budget on mid-range Android, and "calm and fast" is a product value, not a slogan.

markup
<nav class="ns-topnav ns-topnav--dark" aria-label="Dark bar example">
  <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span><span class="ns-topnav__brand-tag">Learn</span></a>
  <ul class="ns-topnav__links">
    <li><a href="#" aria-current="page">Courses</a></li>
    <li><a href="#">Training</a></li>
    <li><a href="#">Docs</a></li>
  </ul>
  <div class="ns-topnav__actions">
    <button type="button" class="ns-navsearch">
      <i class="ph ph-magnifying-glass" aria-hidden="true"></i>
      <span class="ns-navsearch__text">Search courses…</span>
      <kbd class="ns-navsearch__kbd">⌘K</kbd>
    </button>
    <a class="ns-btn ns-btn--white ns-btn--sm" href="#">Start learning</a>
  </div>
</nav>

Floating and sunken

--floating detaches the bar with a gutter and a raised shadow — marketing pages that want air. --sunken drops it onto the sunken surface, which is how it should look above a white content column. Neither belongs in app chrome, where a bar that does not touch the edges wastes the one row of height it costs.

markup
<div style="background:var(--color-surface-sunken);padding-block:var(--space-4)">
  <nav class="ns-topnav ns-topnav--floating" aria-label="Floating example">
    <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span></a>
    <ul class="ns-topnav__links"><li><a href="#" aria-current="page">Courses</a></li><li><a href="#">Pricing</a></li></ul>
    <div class="ns-topnav__actions"><a class="ns-btn ns-btn--primary ns-btn--sm" href="#">Start learning</a></div>
  </nav>
</div>
<nav class="ns-topnav ns-topnav--sunken" aria-label="Sunken example">
  <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span></a>
  <ul class="ns-topnav__links"><li><a href="#" aria-current="page">Courses</a></li><li><a href="#">Pricing</a></li></ul>
  <div class="ns-topnav__actions"><a class="ns-btn ns-btn--outline ns-btn--sm" href="#">Sign in</a></div>
</nav>

Announcement above

News, not chrome — so it sits ABOVE the sticky bar and scrolls away with the page. Pinning it to the viewport is how a banner becomes an ad. --quiet is the same bar on the sunken surface, for a notice that is useful rather than promotional.

New Flow Builder deep-dive — 14 lessons, free this month. Start the course
markup
<div class="ns-announce">
  <span class="ns-announce__kicker">New</span>
  <span class="ns-announce__text">Flow Builder deep-dive — 14 lessons, free this month.</span>
  <a class="ns-announce__link" href="#">Start the course</a>
  <button type="button" class="ns-announce__close" aria-label="Dismiss announcement"><i class="ph ph-x" aria-hidden="true"></i></button>
</div>
<nav class="ns-topnav" aria-label="Announcement example">
  <a class="ns-topnav__brand" href="#"><img src="../assets/logo/favicon.svg" alt=""><span class="ns-topnav__brand-name">Namaste Salesforce</span></a>
  <ul class="ns-topnav__links"><li><a href="#" aria-current="page">Courses</a></li><li><a href="#">Docs</a></li></ul>
  <div class="ns-topnav__actions"><a class="ns-btn ns-btn--primary ns-btn--sm" href="#">Start learning</a></div>
</nav>

The whole thing

Announcement, mega menu, dropdown, search, theme, auth, hamburger and sheet — assembled, at full page width, from templates/navbar.html: open the full navbar demo ↗. Resize below 64rem to get the hamburger and the mobile sheet.

markup
<a class="ns-btn ns-btn--outline" href="./demo-navbar.html">Open the full navbar demo <i class="ph ph-arrow-up-right" aria-hidden="true"></i></a>
<a class="ns-btn ns-btn--outline" href="./demo-navbar-blog.html">Open the blog navbar demo <i class="ph ph-arrow-up-right" aria-hidden="true"></i></a>

Accessibility contract

  • A <nav> landmark with a label, so "skip to navigation" lands somewhere
  • aria-current="page" marks location for assistive tech AND paints the underline — one attribute, both jobs
  • The bar is the first thing in the tab order after the skip link, and the skip link's target carries tabindex="-1"