Nav bar

A brand on one side, destinations on the other, a hairline underneath. It is four classes and no behaviour — which is deliberate, and which means the one thing this page has to be clear about is what happens on a narrow screen.

When to use it

For top-level navigation on a page with a handful of destinations. Above about six it stops fitting and starts needing a menu; below three it is probably just a brand.

For navigation down the side of an application, use the sidebar instead. The two are not variants of each other — they have different classes and different collapse behaviour.

Resize the frame below 768px and the link row disappears
<nav class="navbar" aria-label="Main"> <a class="navbar-brand" href="#"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#grid"></use></svg> Deck </a> <span class="navbar-links"> <a class="nav-link" href="#" aria-current="page">Docs</a> <a class="nav-link" href="#">Components</a> <a class="nav-link" href="#">Tokens</a> </span> <span class="push"></span> <button type="button" class="btn btn-sm">Sign in</button> </nav>
<nav class="navbar" aria-label="Main">
  <a class="navbar-brand" href="#">
    <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#grid"></use></svg>
    Deck
  </a>
  <span class="navbar-links">
    <a class="nav-link" href="#" aria-current="page">Docs</a>
    <a class="nav-link" href="#">Components</a>
    <a class="nav-link" href="#">Tokens</a>
  </span>
  <span class="push"></span>
  <button type="button" class="btn btn-sm">Sign in</button>
</nav>

The bar is a plain flex row with a gap, so ordering and spacing are yours. .push is the usual way to shove everything after it to the far end.

What happens on a narrow screen

.navbar-links is display: none and only becomes a flex row at 48rem. Below that the destinations are gone — not collapsed, not stacked, not behind a button. Deck does not supply the replacement.

.navbar-links { display: none; gap: var(--space-1); align-items: center; }
@media (min-width: 48rem) { .navbar-links { display: flex; } }

That is a reasonable division of labour — there are three or four good answers and the right one depends on the application — but it means a .navbar on its own is an unfinished component on a phone. The three answers Deck already has parts for:

  • A drawer opened by a button in the bar. The usual choice for a site with more than a few destinations.
  • A sheet from the bottom, which is easier to reach one-handed on a tall phone.
  • A tab bar pinned to the bottom for an application with three to five top-level areas.
The simplest version: a button that opens a popover menu, hidden above the breakpoint
<nav class="navbar" aria-label="Main"> <a class="navbar-brand" href="#">Deck</a> <span class="navbar-links"> <a class="nav-link" href="#" aria-current="page">Docs</a> <a class="nav-link" href="#">Components</a> </span> <span class="push"></span> <button type="button" class="btn btn-icon btn-ghost md:hidden" aria-label="Open navigation" popovertarget="dx-nav-menu"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#menu"></use></svg> </button> <div class="menu" id="dx-nav-menu" popover> <a class="menu-item" href="#">Docs</a> <a class="menu-item" href="#">Components</a> </div> </nav>
<nav class="navbar" aria-label="Main">
  <a class="navbar-brand" href="#">Deck</a>
  <span class="navbar-links">
    <a class="nav-link" href="#" aria-current="page">Docs</a>
    <a class="nav-link" href="#">Components</a>
  </span>
  <span class="push"></span>
  <button type="button" class="btn btn-icon btn-ghost md:hidden" aria-label="Open navigation"
          popovertarget="dx-nav-menu">
    <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#menu"></use></svg>
  </button>
  <div class="menu" id="dx-nav-menu" popover>
    <a class="menu-item" href="#">Docs</a>
    <a class="menu-item" href="#">Components</a>
  </div>
</nav>

Whatever you choose, the destinations must exist twice — once in .navbar-links for wide screens and once in the disclosure for narrow ones — or be moved between them. Two copies of the same list is a maintenance cost worth knowing about before you build it.

.nav-link is one destination. It is muted until hovered, and [aria-current] gives it the brand colour on a soft brand background — note the bare attribute selector, so aria-current="page", "true" or any other value all match.

The source carries an instruction worth repeating: use .btn only for actions, never for navigation. A link goes somewhere and can be opened in a new tab, copied, or followed with the keyboard as a link. A button does something. Styling one as the other takes that away from the reader for no gain.

The current page is coloured and, more importantly, carries aria-current
<span class="cluster cluster-tight"> <a class="nav-link" href="#" aria-current="page">Overview</a> <a class="nav-link" href="#">Activity</a> <a class="nav-link" href="#">Settings</a> </span>
<span class="cluster cluster-tight">
  <a class="nav-link" href="#" aria-current="page">Overview</a>
  <a class="nav-link" href="#">Activity</a>
  <a class="nav-link" href="#">Settings</a>
</span>

Classes

Generated from src/07-components.css by tools/docs/extract.mjs.

Classes, generated from the stylesheet source
Class Layer What it does Source
.navbar deck.components Sets display, align-items, gap, min-block-size, and 2 more. 07-components.css:810
.navbar-brand deck.components Sets display, align-items, gap, font-weight, and 4 more. 07-components.css:818
.navbar-links deck.components Sets display, gap, align-items. 07-components.css:828
.nav-link deck.components One destination in a .navbar. Carries the hover and current-page states; use .btn only for actions, never for navigation. 07-components.css:832

Tokens

Design tokens this component reads
Token Live Declared value
--control-h-lg 50px
--line light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral)))
--text-md 1.0625rem
--text-sm .8125rem
--text-muted light-dark(var(--ink-600), var(--ink-400))
--surface-hover light-dark(var(--ink-50), var(--ink-900))
--brand light-dark(var(--brand-600), var(--brand-400))
--brand-soft light-dark(var(--brand-50), oklch(26% .05 var(--hue-brand)))
--r-sm 7px
--dur-1 110ms

Accessibility

  • Put it in a <nav> with a label. .navbar is a class, not an element — it confers no role. A page with more than one navigation region needs each labelled, or they are announced as identical.
  • Mark the current page. aria-current="page" is what tells a screen reader where the reader is. The colour is the sighted half of the same signal, and Deck keys its styling off the attribute so the two cannot drift apart.
  • Below 48rem the links leave the accessibility tree. display: none removes them for everybody — this is not a visual-only hide. A screen reader user on a phone has no navigation at all unless you have built the disclosure described above. It is the most consequential thing on this page.
  • The brand should be a link to the home page and should say so. A logo with no text is announced as nothing; give the <a> an accessible name even where the mark is enough for a sighted reader.
  • Links are links. Do not attach click handlers to <span> elements and style them .nav-link. Keyboard users navigate by link, and a fake one is invisible to that.
  • The bar does not stick by itself. If you make it position: sticky, remember it then covers content that anchors scroll to — add scroll-margin-block-start to your headings, or a skipped-to heading lands underneath the bar.

Right to left

The bar is a flex row and the link padding is symmetrical, so the whole thing mirrors with the document — brand on the right, destinations running leftward — with nothing in src/19-logical.css required. The one thing to check is your own .push, which uses margin-inline-start: auto and so also follows direction correctly.

<nav dir="rtl" class="navbar" aria-label="التنقل الرئيسي"> <a class="navbar-brand" href="#">ديك</a> <span class="navbar-links"> <a class="nav-link" href="#" aria-current="page">المستندات</a> <a class="nav-link" href="#">المكوّنات</a> </span> </nav>
<nav dir="rtl" class="navbar" aria-label="التنقل الرئيسي">
  <a class="navbar-brand" href="#">ديك</a>
  <span class="navbar-links">
    <a class="nav-link" href="#" aria-current="page">المستندات</a>
    <a class="nav-link" href="#">المكوّنات</a>
  </span>
</nav>

Reduced motion

.nav-link transitions its background and text colour over 110ms on hover, and the global reset in src/02-reset.css collapses that to .01ms. Nothing else on the bar moves. If you add a sticky bar that hides on scroll, that motion is yours to guard.

Printing

.navbar-links is in the "chrome that should never print" list in src/99-print.css, so the destinations are dropped. The bar itself and the brand remain, which is the right split: the brand identifies the document, and a list of places you cannot click is noise.

.nav-link is not in that list, so a .nav-link used outside .navbar-links will still print. If you have a secondary row of them, add it to your own print rule or give it .no-print.

Overriding it

@layer app.components {
  /* Show the links earlier than 48rem */
  @media (min-width: 40rem) { .navbar-links { display: flex; } }

  /* A sticky bar — remember the scroll-margin on your headings */
  .navbar {
    position: sticky;
    inset-block-start: 0;
    background: var(--surface);
    z-index: 10;
  }
  :target, h2 { scroll-margin-block-start: 4rem; }

  /* An underline for the current page instead of a filled pill */
  .nav-link[aria-current] {
    background: none;
    box-shadow: inset 0 -2px 0 var(--brand);
    border-radius: 0;
  }
}

When not to use it

  • Not on a phone without a disclosure. The link row is hidden and nothing takes its place. This is the failure mode of this component.
  • Not for more than about six destinations. They will not fit, and the row will not wrap gracefully. Group them behind a menu.
  • Not for navigation within a page. Sections of the current document are tabs or a table of contents, not top-level navigation.
  • Not for an application shell. A left rail with grouped destinations is a sidebar; it collapses gracefully and this does not.
  • Not with .btn for the destinations. See .nav-link. Buttons for actions, links for places.