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.
<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.
<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, and when not to use a button
.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.
<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.
| 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
| 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..navbaris 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: noneremoves 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 — addscroll-margin-block-startto 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
.btnfor the destinations. See .nav-link. Buttons for actions, links for places.