Navigation
Building consistent navigation patterns with links and buttons
Navigation patterns cover how users move through an interface. Each pattern follows the same accessibility principles and uses the same link primitive — ButtonLink — to keep visual language consistent.
Nav bar
A horizontal navigation bar for top-level site sections. Use <nav> as the landmark, mark the active page with aria-current="page", and use ghost ButtonLink elements so the links match the design system focus ring.
<nav aria-label="Main">
<ul class="flex list-none gap-1 p-0">
<li>
<ButtonLink href="/" variant="outlined" size="sm" aria-current="page">Home</ButtonLink>
</li>
<li>
<ButtonLink href="/foundations" variant="ghost" size="sm">Foundations</ButtonLink>
</li>
<li>
<ButtonLink href="/components" variant="ghost" size="sm">Components</ButtonLink>
</li>
<li>
<ButtonLink href="/patterns" variant="ghost" size="sm">Patterns</ButtonLink>
</li>
</ul>
</nav>Rules:
- Always wrap the list in a
<nav>landmark. - The active link receives
aria-current="page"— do not use only a visual active state. - Do not use primary
ButtonLinkvariants in nav bars — a fill draws too much attention for a list of destinations. Ghost is the default; outlined marks the current one.
Button vs link
The single most important navigation decision is choosing the right element.
| Use | Element | Why |
|---|---|---|
| Navigate to a URL | <a> / ButtonLink | Has an href; browser history, open-in-tab, right-click all work correctly |
| Trigger an action | <button> / Button | No href; activates JS behavior without changing the URL |
Rule: Never use a <button> to navigate and never use an <a> to trigger an action. Swapping them breaks keyboard users, screen readers, and browser affordances.
<!-- Navigate to a page — use ButtonLink (<a>) -->
<ButtonLink href="/settings">Settings</ButtonLink>
<!-- Trigger an action — use Button (<button>) -->
<Button variant="primary" type="submit">Save changes</Button>See ButtonLink for the full props API.
Breadcrumbs
Breadcrumbs show the user’s location in a page hierarchy. Use <nav aria-label="Breadcrumb"> with an ordered list. Separate crumbs with a visible, aria-hidden separator. The last item represents the current page and should not be a link.
<nav aria-label="Breadcrumb">
<ol class="flex list-none items-center gap-1 p-0">
<li>
<ButtonLink href="/" variant="ghost" size="sm">Home</ButtonLink>
</li>
<li aria-hidden="true" class="text-control-sm text-dark/55">/</li>
<li>
<ButtonLink href="/components" variant="ghost" size="sm">Components</ButtonLink>
</li>
<li aria-hidden="true" class="text-control-sm text-dark/55">/</li>
<li>
<span aria-current="page" class="text-control-sm px-2 text-dark">Button</span>
</li>
</ol>
</nav>Rules:
- Use
<ol>— breadcrumbs are ordered by hierarchy. - The separator (
/) is purely decorative; addaria-hidden="true"so screen readers skip it. - The current page item uses
aria-current="page"and is plain text, not a link — linking to the current page is redundant.
Pagination
Pagination lets users move between pages of a data set. Previous and next are ghost ButtonLink elements; page numbers use ghost, with the current page outlined. The active page is marked aria-current="page".
<nav aria-label="Pagination">
<ul class="flex list-none items-center gap-1 p-0">
<li>
<ButtonLink href="?page=1" variant="ghost" size="sm" aria-label="Previous page">← Prev</ButtonLink>
</li>
<li>
<ButtonLink href="?page=1" variant="ghost" size="sm" aria-label="Page 1">1</ButtonLink>
</li>
<li>
<ButtonLink href="?page=2" variant="outlined" size="sm" aria-current="page" aria-label="Page 2, current">2</ButtonLink>
</li>
<li>
<ButtonLink href="?page=3" variant="ghost" size="sm" aria-label="Page 3">3</ButtonLink>
</li>
<li>
<ButtonLink href="?page=3" variant="ghost" size="sm" aria-label="Next page">Next →</ButtonLink>
</li>
</ul>
</nav>Rules:
- Wrap in
<nav aria-label="Pagination">so the landmark is announced distinctly from other navs. - Each page number link must have a descriptive
aria-label(e.g."Page 3") — numeric-only link text is ambiguous out of context. - The active page item receives
aria-current="page"and a higher-emphasis variant (outlined) to distinguish it visually without relying on color alone. - Disable or hide the previous link on page 1 and the next link on the last page.
Tab bar
A CSS-only tab strip for switching between views within the same page context. Use role="tablist" / role="tab" / role="tabpanel" and aria-selected to communicate state. No JavaScript is required when content is always present; add a script only when panels need to show and hide.
<div>
<div role="tablist" aria-label="Account sections" class="flex border-b border-dark/12">
<a href="#tab-profile" role="tab" aria-selected="true" aria-controls="tab-profile"
class="text-control-sm -mb-px border-b-2 border-primary px-4 py-2 text-dark no-underline
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
Profile
</a>
<a href="#tab-billing" role="tab" aria-selected="false" aria-controls="tab-billing"
class="text-control-sm -mb-px border-b-2 border-transparent px-4 py-2 text-dark/55 no-underline
hover:text-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
Billing
</a>
<a href="#tab-security" role="tab" aria-selected="false" aria-controls="tab-security"
class="text-control-sm -mb-px border-b-2 border-transparent px-4 py-2 text-dark/55 no-underline
hover:text-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
Security
</a>
</div>
<div id="tab-profile" role="tabpanel" aria-label="Profile" class="text-small p-4 text-dark/70">
Profile panel content.
</div>
</div>Rules:
- The active tab is indicated by
aria-selected="true"and a bottom border inbg-primary— never color alone. - Use
<a>tabs when each tab maps to a URL (deep-linkable); use<button>tabs when content is toggled in place with JavaScript. - Inactive tabs use
text-dark/55and a transparent bottom border to show the same structural affordance at a lower emphasis. - Each tab names its panel with
aria-controls, and the panel carriesrole="tabpanel"with a matchingid. - Add keyboard arrow-key navigation when using JavaScript-toggled panels (Left/Right to move focus, Home/End for first/last), and a roving
tabindexso the strip is one stop rather than three.
Accessibility
Landmark roles
Wrap every navigation region in a <nav> element. This creates a landmark that screen reader users can jump to directly.
<nav aria-label="Main">…</nav>
<nav aria-label="Breadcrumb">…</nav>
<nav aria-label="Pagination">…</nav>Multiple nav landmarks
When a page contains more than one <nav>, each must have a distinct aria-label so users can tell them apart. Generic labels like "Navigation" are unhelpful when there are two.
<!-- Two navs on the same page — both need distinct labels -->
<nav aria-label="Main">…site-wide links…</nav>
<nav aria-label="On this page">…in-page anchor links…</nav>Keyboard focus
- All
ButtonLinkelements use:focus-visiblewithoutline-primary— never suppress the outline. - Tab order must follow visual reading order. Do not use positive
tabindexvalues to reorder focus. - For JavaScript-driven tab bars, implement arrow-key navigation within the
tablistso keyboard users do not have to Tab through every tab to reach the panel. - Skip links (e.g. “Skip to main content”) should appear as the first focusable element on pages with long navigation blocks.
<!-- Skip link — first element in <body> -->
<a href="#main-content" class="skip-link">Skip to main content</a>