Pattern
Navigation
Overview
Monogem ships six separate navigation primitives — Navbar,
Sidebar Nav, Bottom Nav,
Breadcrumb, Tabs, and
Pagination — each documenting its own anatomy, states, and
contract in isolation. None of them says how a real product combines two or more of them on the
same screen. This is a composition pattern, not a seventh primitive: Monogem ships no
<Navigation> component. It is decision guidance — which
primitive answers which navigation job, and how the already-documented primitives combine — plus a
live example that composes them unchanged.
No change to any of the six primitives — each keeps its documented contract exactly as written. This page cites Navbar, Sidebar Nav, Bottom Nav, Breadcrumb, Tabs, and Pagination; it never redefines them. It also invents no application routing architecture and no product-specific information architecture — “which page is current” and “what the sections are called” stay the caller's decision in every composition below, the same deferral each primitive already makes on its own page.
Anatomy
Every navigation primitive already answers "reach for something else when the job is different" on its own page. This table collects those individual answers in one place:
| Primitive | Answers | Scope | Changes the route? |
|---|---|---|---|
| Navbar | "How do I get into this site/app?" | Global, top-of-page, every width (collapses itself below md) | Yes — links navigate |
| Sidebar Nav | "What are this app's primary sections?" | App-shell, persistent, desktop/tablet | Yes — items navigate |
| Bottom Nav | Same job as Sidebar Nav, mobile-only | App-shell, fixed to the viewport, below md only | Yes — items navigate |
| Breadcrumb | "Where am I in the hierarchy, and how do I step back up?" | Secondary, in-page, orientation only | Yes for ancestor links — the current page itself is a non-link <span> |
| Tabs | "Which view of this same page/content?" | Same-page, no navigation | No — same URL, same route |
| Pagination | "Which page of this bounded result set?" | Same-page-type, list context | Usually — a real page route (?page=3) |
Two axes fall out of this table and explain most of the guidance below:
- Primary vs. secondary. Navbar, Sidebar Nav, and Bottom Nav are the way a user enters or moves between an app's major sections — a product needs at least one. Breadcrumb, Tabs, and Pagination are never a product's only navigation; they supplement whatever primary nav already got the user to the current page — Breadcrumb's own Overview makes this explicit ("it supplements primary nav, it doesn't replace it").
- Navigates vs. stays put. Navbar, Sidebar Nav, Bottom Nav, Breadcrumb's ancestor links, and Pagination all change what page or route is loaded. Tabs never does — see tabs.md's own "Tabs vs. navigation links" table. It is the one primitive in this set that is not, strictly, navigation at all, which is why reaching for it to build primary site navigation is called out as a misuse on its own page.
Live Example
Global + local navigation, with hierarchy and same-page views
- Draft the onboarding checklist
- Review the Q3 roadmap
Navbar carries no destination links here — Sidebar Nav already owns “which section,” so the top bar stays limited to the brand and an always-visible action. The Breadcrumb trail and the selected Tabs trigger are each their own “current” signal, independent of Sidebar Nav’s current section.
Responsive substitution — rail hands off to a bottom tab bar
Wide (desktop/tablet) — Sidebar Nav rail
Narrow (mobile) — Bottom Nav bar
Page content
Both frames render the same four destinations, in the same order, and share one “current” value — activating an item in either frame moves both. This is a static stand-in for the hand-off; in real usage the rail is `md:block` and Bottom Nav is `md:hidden`, so only one is ever actually visible at a given viewport width (Bottom Nav’s own real `fixed` / `md:hidden` classes are also overridden here, the same demo-only override its own live example uses).
Code Example
"use client";
import type { MouseEvent } from "react";
import { useState } from "react";
import { Activity, Compass, FolderKanban, Home, LayoutDashboard, Settings } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Navbar, NavbarActions, NavbarBrand, NavbarContent } from "@/components/ui/navbar";
import {
SidebarNav,
SidebarNavGroup,
SidebarNavItem,
} from "@/components/ui/sidebar-nav";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@/components/ui/tabs";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav";
/**
* Live Navigation example — every part is a real, unmodified primitive from
* its own doc; this page adds no components/ui/navigation.tsx of any kind.
* `"use client"` only so the demo can swallow real navigation and hold
* current-page / tab / pagination state — every part rendered is itself a
* server component.
*/
const stayOnPage = (event: MouseEvent<HTMLAnchorElement>) => event.preventDefault();
const TASKS = [
"Draft the onboarding checklist",
"Review the Q3 roadmap",
"Fix the broken invite link",
"Write release notes",
"Pair on the billing migration",
"Triage the support backlog",
];
const TASKS_PER_PAGE = 2;
const TASK_PAGE_COUNT = Math.ceil(TASKS.length / TASKS_PER_PAGE);
/**
* Section 1 — global + local navigation composed with hierarchy (Breadcrumb)
* and same-page views (Tabs), one of which pages a list (Pagination).
* Navbar renders no `NavbarLinks` / `NavbarMobileMenu` here — Sidebar Nav
* already owns "which section," per docs/patterns/navigation.md's "Global
* and local navigation."
*/
function GlobalAndLocalExample() {
const [taskPage, setTaskPage] = useState(1);
const pageTasks = TASKS.slice(
(taskPage - 1) * TASKS_PER_PAGE,
taskPage * TASKS_PER_PAGE,
);
return (
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Global + local navigation, with hierarchy and same-page views
</h3>
<div className="overflow-hidden rounded-md border border-solid border-border">
<Navbar className="static">
<NavbarContent>
<NavbarBrand>
<span className="rounded-md px-1 py-1.5 text-sm font-semibold leading-snug tracking-tight">
Acme
</span>
</NavbarBrand>
<NavbarActions>
<Button variant="outline" size="sm">
Account
</Button>
</NavbarActions>
</NavbarContent>
</Navbar>
<div className="flex items-stretch">
<aside className="w-48 shrink-0 bg-sidebar p-3 text-sidebar-foreground">
<SidebarNav aria-label="Product">
<SidebarNavGroup label="Workspace">
<SidebarNavItem
href="/dashboard"
icon={<LayoutDashboard />}
onClick={stayOnPage}
>
Dashboard
</SidebarNavItem>
<SidebarNavItem
href="/projects"
current
icon={<FolderKanban />}
onClick={stayOnPage}
>
Projects
</SidebarNavItem>
<SidebarNavItem
href="/settings"
icon={<Settings />}
onClick={stayOnPage}
>
Settings
</SidebarNavItem>
</SidebarNavGroup>
</SidebarNav>
</aside>
<div className="flex-1 p-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/dashboard" onClick={stayOnPage}>
Dashboard
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/projects" onClick={stayOnPage}>
Projects
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Acme redesign</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="mt-4">
<Tabs defaultValue="tasks">
<TabsList aria-label="Project views" className="overflow-y-hidden">
<TabsTrigger value="tasks">Tasks</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
<TabsPanel value="tasks" className="flex flex-col gap-3 pt-3">
<ul className="flex flex-col gap-1">
{pageTasks.map((task) => (
<li
key={task}
className="rounded-sm px-2 py-1.5 text-sm leading-normal text-foreground"
>
{task}
</li>
))}
</ul>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${taskPage - 1}`}
disabled={taskPage === 1}
onClick={(event) => {
event.preventDefault();
setTaskPage((page) => Math.max(1, page - 1));
}}
/>
</PaginationItem>
{Array.from({ length: TASK_PAGE_COUNT }, (_, i) => i + 1).map(
(page) => (
<PaginationItem key={page}>
<PaginationLink
href={`?page=${page}`}
isActive={taskPage === page}
onClick={(event) => {
event.preventDefault();
setTaskPage(page);
}}
>
{page}
</PaginationLink>
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext
href={`?page=${taskPage + 1}`}
disabled={taskPage === TASK_PAGE_COUNT}
onClick={(event) => {
event.preventDefault();
setTaskPage((page) =>
Math.min(TASK_PAGE_COUNT, page + 1),
);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</TabsPanel>
<TabsPanel
value="activity"
className="pt-3 text-sm leading-normal text-muted-foreground"
>
Recent activity on this project — comments, status changes,
and assignments, newest first.
</TabsPanel>
</Tabs>
</div>
</div>
</div>
</div>
<p className="text-xs leading-snug text-muted-foreground">
Navbar carries no destination links here — Sidebar Nav already owns
“which section,” so the top bar stays limited to the brand and an
always-visible action. The Breadcrumb trail and the selected Tabs
trigger are each their own “current” signal, independent of Sidebar
Nav’s current section.
</p>
</section>
);
}
const SHELL_ITEMS = [
{ href: "/home", label: "Home", icon: <Home /> },
{ href: "/explore", label: "Explore", icon: <Compass /> },
{ href: "/activity", label: "Activity", icon: <Activity /> },
{ href: "/settings", label: "Settings", icon: <Settings /> },
];
/**
* Section 2 — the same app-shell destination list rendered as both forms
* named in docs/patterns/navigation.md's "Desktop-to-mobile transitions":
* a Sidebar Nav rail (wide) and a Bottom Nav bar (narrow). One shared
* `current` state drives both frames, standing in for the real `md:block` /
* `md:hidden` breakpoint hand-off Sidebar Nav's own and Bottom Nav's own
* responsive examples each demonstrate individually against the actual
* viewport.
*/
function ResponsiveSubstitutionExample() {
const [current, setCurrent] = useState(SHELL_ITEMS[0].href);
return (
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Responsive substitution — rail hands off to a bottom tab bar
</h3>
<div className="flex flex-wrap gap-6">
<div className="flex flex-col gap-2">
<p className="text-xs leading-snug text-muted-foreground">
Wide (desktop/tablet) — Sidebar Nav rail
</p>
<div className="h-64 w-48 overflow-hidden rounded-md border border-solid border-border bg-sidebar p-3 text-sidebar-foreground">
<SidebarNav aria-label="Product">
<SidebarNavGroup>
{SHELL_ITEMS.map((item) => (
<SidebarNavItem
key={item.href}
href={item.href}
icon={item.icon}
current={item.href === current}
onClick={(event) => {
stayOnPage(event);
setCurrent(item.href);
}}
>
{item.label}
</SidebarNavItem>
))}
</SidebarNavGroup>
</SidebarNav>
</div>
</div>
<div className="flex flex-col gap-2">
<p className="text-xs leading-snug text-muted-foreground">
Narrow (mobile) — Bottom Nav bar
</p>
<div className="relative h-64 w-80 overflow-hidden rounded-md border border-solid border-border bg-background">
<div className="flex h-full flex-col gap-2 p-3 pb-20">
<p className="text-xs leading-snug text-muted-foreground">
Page content
</p>
</div>
<BottomNav className="absolute md:flex">
{SHELL_ITEMS.map((item) => (
<BottomNavItem
key={item.href}
href={item.href}
icon={item.icon}
label={item.label}
current={item.href === current}
onClick={(event) => {
stayOnPage(event);
setCurrent(item.href);
}}
/>
))}
</BottomNav>
</div>
</div>
</div>
<p className="text-xs leading-snug text-muted-foreground">
Both frames render the same four destinations, in the same order, and
share one “current” value — activating an item in either frame moves
both. This is a static stand-in for the hand-off; in real usage the
rail is `md:block` and Bottom Nav is `md:hidden`, so only one is ever
actually visible at a given viewport width (Bottom Nav’s own real
`fixed` / `md:hidden` classes are also overridden here, the same
demo-only override its own live example uses).
</p>
</section>
);
}
export function NavigationShowcase() {
return (
<div className="flex flex-col gap-10">
<GlobalAndLocalExample />
<ResponsiveSubstitutionExample />
</div>
);
}Usage Guidance
Choosing a primitive
- A user has just arrived, or needs to reach the app/site at all → Navbar. It is the only primitive here designed to also work as a marketing/site-wide header (brand + links + actions), and the only one that bakes in its own full mobile collapse (hamburger + Sheet) rather than asking the caller to compose one.
- The user is already inside an app and needs to move between its primary sections →
Sidebar Nav on desktop/tablet,
Bottom Nav on mobile — see
Desktop-to-mobile transitions for how the two relate. Reach for
Sidebar Nav specifically when destinations benefit from grouping (its own
SidebarNavGroup); reach for Bottom Nav specifically when the product is mobile/app-style and destinations fit 3–5 thumb-reachable items (bottom-nav.md's own "Do" guidance). - The user needs to know where the current page sits in a hierarchy, or step back up it → Breadcrumb. Only worth adding when nesting is more than one level deep and stable — skip it for a flat site or a single-level hierarchy, per breadcrumb.md's own Overview.
- The user is switching between equal-standing views of the same subject, with no route change → Tabs. If the destinations are genuinely different pages, that's navigation links (possibly inside a Navbar or Sidebar Nav), not Tabs.
- The user is paging through a long, bounded, ordered result set → Pagination. Not for an endless feed (infinite scroll) or a small list that fits on one page, per pagination.md's own Overview.
Common compositions
Global and local navigation
A product with both a site-wide shell (marketing pages, sign-in, account switching) and an
app-shell with its own sections composes Navbar for the former and
Sidebar Nav (or Bottom Nav below
md) for the latter — never one primitive trying to do both jobs. The usual split, and the one the
Live Example above builds:
- Navbar stays thin once the user is inside the app — a brand mark linking back to the app
root, plus
NavbarActions(a theme toggle, an account control). It renders noNavbarLinksin this composition — Sidebar Nav already owns "which section," so a second, competing list of the same destinations in the top bar would give the user two places to look for one decision. - Sidebar Nav carries the app's actual primary destinations, grouped where that helps.
- Both mark "current" independently, from the same caller-computed route match — Navbar has no current link in this split (it has no destination links to mark); Sidebar Nav marks the current section.
A marketing site with no app-shell just uses Navbar alone with its NavbarLinks populated, per
navbar.md's own baseline. A single-surface app with no separate marketing shell just uses Sidebar
Nav (or Bottom Nav) alone. The "both, split by role" composition above is specifically for a
product that is both.
Desktop-to-mobile transitions
The three primary-navigation primitives each handle a narrow viewport differently, by design — this pattern doesn't reconcile them into one behavior, it explains which one to reach for:
| Primitive | Below md | Caller composition needed? |
|---|---|---|
| Navbar | Bakes in its own collapse — NavbarLinks hides, NavbarMobileMenu (hamburger + Sheet) appears in its place, in the same tree | No — Navbar owns this entirely (see navbar.md's Responsive contract) |
| Sidebar Nav | Renders a static list at every width; no built-in breakpoint | Yes — the caller composes a persistent rail (md:block) alongside the same SidebarNav tree rendered inside a Sheet, following Sidebar Nav's own documented shape |
| Bottom Nav | Bakes in its own visibility — fixed, md:hidden | No — mount it once at the shell level; it hides itself (see bottom-nav.md's Responsive contract) |
The common product pattern that falls out of this table is Sidebar Nav paired with Bottom Nav:
the rail renders md:block, Bottom Nav renders itself at the same breakpoint's other side
(md:hidden is its own built-in behavior) — together they give an app-shell primary navigation
that's present at every width without either primitive changing shape into the other. Bottom Nav's
own doc names this explicitly: "A product that needs primary navigation at wider viewports mounts
Navbar or Sidebar Nav alongside it." The Live Example's second section demonstrates
the rail/bar hand-off side by side.
A Navbar-based marketing shell does not need a Bottom Nav counterpart — Navbar's own hamburger + Sheet is already its complete mobile answer; pairing it with Bottom Nav would give a narrow viewport two competing primary-navigation surfaces — see When they coexist vs replace one another.
Communicating current location
Every primitive in this set that navigates marks "where the user is" the same way — one consistent signal a caller writes once and gets right everywhere:
aria-current="page", plus a text-weight or fill change, never color alone — Navbar, Sidebar Nav, Bottom Nav, Breadcrumb'sBreadcrumbPage, and Pagination'sisActivelink all use this exact pair (see each primitive's own States / Accessibility sections). In every case it's the caller's job to compute which item is current (a router match,usePathname) — no primitive in this set does route matching itself.- Tabs is the one exception, deliberately so — it isn't marking a page location, it's marking a
selected view:
aria-selectedplusaria-controls/aria-labelledbybetween the trigger and its panel (see tabs.md's Accessibility), notaria-current. Don't reach foraria-currentinside a Tabs composition; it isn't the right signal for a same-page view switch. - A page composing several of these together (the Live Example's combined
section) ends up with more than one "current" marker on screen at once, each scoped to its own
landmark — Sidebar Nav's current section, a Breadcrumb's current page, and a Tabs' selected
trigger are three independent, simultaneously-true facts, not competing claims. Each comes from a
different piece of state (route section, full path, selected tab value), and each primitive's own
landmark (
<nav aria-label="…">orrole="tablist") keeps them from being confused with one another by assistive tech.
Hierarchy
Primary navigation (Navbar / Sidebar Nav / Bottom Nav) answers "which section" — a shallow, usually one-level choice. Breadcrumb answers "how deep, and through what path" once the user is inside a section whose own pages nest further. The two never substitute for each other — a Breadcrumb with no primary nav behind it has nothing to be a supplement to (breadcrumb.md's own Overview and Usage sections both make this explicit), and a Sidebar Nav item is not a substitute for a multi-level trail once a section's own pages nest more than one level deep. Composing them together (Sidebar Nav in the shell, Breadcrumb at the top of the content region) is the default shape for a documentation site, a settings area, or a catalog — the shape the Live Example builds.
When they coexist vs replace one another
| Pair | Coexist or replace | Why |
|---|---|---|
| Navbar + Sidebar Nav | Coexist, different roles | Global entry vs. app-local sections — see Global and local navigation |
| Sidebar Nav + Bottom Nav | Replace each other by breakpoint, same role | Both answer "which primary section" — one is the desktop/tablet form, the other the mobile form, of the same decision. Mount both; only one is ever visible at a time. |
| Navbar + Bottom Nav | Usually replace, same underlying role, if Navbar is standing in for Sidebar Nav's job below md | Navbar already has a complete built-in mobile answer (its own hamburger + Sheet); adding Bottom Nav alongside it gives a narrow viewport two competing "primary navigation" surfaces. A shell that wants a thumb-reachable tab bar instead of a hamburger on mobile reaches for Bottom Nav in Sidebar Nav's place, not in addition to Navbar's own collapse. |
| Any primary nav + Breadcrumb | Coexist, different roles | Primary nav picks the section; Breadcrumb shows depth within it — see Hierarchy |
| Any primary nav + Tabs | Coexist, different roles | Tabs operate inside a page already reached through primary nav; it never picks the page itself |
| Breadcrumb + Tabs | Coexist, different axes | Hierarchy (where nested) vs. same-page views (what's shown) — both can sit on one page, breadcrumb above, tabs below, as in the Live Example |
| Pagination + anything above | Coexist, unrelated scope | Pagination pages through a list inside whatever content region the surrounding nav already brought the user to |
The one rule that falls out of every row: don't give a single viewport two primitives competing for the same "which primary section" decision. Everything else in this set answers a distinct enough question that composing it alongside another primitive is the normal, expected shape, not an edge case.
What the example composes
The example below composes every primitive on this page unchanged, in the two shapes named above:
- Global and local navigation, with hierarchy and same-page views — a thin Navbar (brand + one
action, no
NavbarLinks) over a Sidebar Nav rail and a content region that carries its own Breadcrumb trail and a Tabs switcher, one of whose panels pages a result list with Pagination. - Responsive substitution — the same app-shell destinations rendered twice, side by side: as a
Sidebar Nav rail (the wide-viewport form) and as a Bottom Nav bar (the narrow-viewport form),
both marking the same destination current — a static side-by-side stand-in for the real
md:block/md:hiddenbreakpoint hand-off Sidebar Nav's own and Bottom Nav's own responsive examples each demonstrate individually.
Spacing
No new spacing values. Layout gaps between composed primitives in the live example are ordinary
Spacing rhythm already used elsewhere on the site — --scale-6
between the sidebar rail and the content column, --scale-4 between a Breadcrumb trail and the
Tabs below it. Each composed primitive's own internal spacing (a NavbarLink's padding, a
SidebarNavItem's row height, a PaginationLink's size-10 footprint) is untouched, defined on
its own page.
Tokens
Every value in the Live Example comes from the six primitives' own already-documented token families, unchanged:
| Family | Where | Doc |
|---|---|---|
--background / --border / --muted-foreground / --foreground / --ring | Navbar's chrome and link states | navbar.md |
--sidebar / --sidebar-foreground / --sidebar-primary / --sidebar-accent / --sidebar-ring | Sidebar Nav's surface and item states | sidebar-nav.md |
--background / --border / --muted-foreground / --foreground / --ring | Bottom Nav's chrome and item states | bottom-nav.md |
--muted-foreground / --foreground / --ring | Breadcrumb's link and current-page states | breadcrumb.md |
--foreground / --muted-foreground / --primary / --border / --ring | Tabs' selected/unselected trigger states | tabs.md |
--accent / --accent-foreground / --border / --background / --foreground / --ring | Pagination's link states, via buttonVariants | pagination.md |
Do / Don’t
Do
- Pick exactly one primitive per navigation role on a given screen — one "which section" answer, one "where in the hierarchy" answer, and so on — per When they coexist vs replace one another.
- Give Sidebar Nav and Bottom Nav the same destination list, in the same order, when a product pairs them across breakpoints — the same "one destination list rendered twice, not two information architectures" rule Navbar's own Usage section states for its own desktop/mobile split.
- Compute "current" independently for each composed primitive, from the same underlying route
state, and let each mark it with its own documented
aria-current/aria-selectedtreatment — see Communicating current location. - Let Breadcrumb and Tabs coexist freely with primary nav and with each other — they answer different questions, and neither owns "which primary section."
- Keep this page's guidance to composition — for a single primitive's own anatomy, states, or accessibility contract, defer to that primitive's own doc; don't re-derive it here.
Don’t
- Build a
<Navigation>wrapper around two or more of these primitives — the same rejection Search and Empty States make for their own wrapper components. - Render two primitives that both claim to answer "which primary section" at the same viewport
width (e.g. a populated
NavbarLinksand a visible Bottom Nav at once) — see When they coexist vs replace one another. - Reach for Tabs to build primary site navigation, or Breadcrumb as a site's only navigation — both are explicitly called out as misuse on their own pages.
- Invent an active-route matcher, a notification/badge data source, or any other product-specific information architecture at this layer — every primitive in this set already defers "which item is current" and similar state to the caller; this pattern page doesn't override that deferral.
- Duplicate a primitive's documented contract here instead of linking to it — if a detail isn't specific to composing two primitives together, it belongs on that primitive's own page, not this one.
Accessibility
- Multiple
<nav>landmarks on one screen need distinct names. Navbar, Sidebar Nav, Bottom Nav, Breadcrumb, and Pagination each render their own<nav>with a sensible defaultaria-label("Primary", "Sidebar", "Breadcrumb", "Pagination") — when a composition like Global and local navigation puts more than one on screen at once, give each a distinct label (every primitive's own doc already documents this override) so a screen-reader user's landmark list distinguishes "jump to primary navigation" from "jump to the breadcrumb." aria-currentstays singular per landmark, never global. Each<nav>in a composition marks its own current item independently — Sidebar Nav's current section and Breadcrumb's current page are two separatearia-current="page"occurrences in two different landmarks, not a conflict; the "exactly one per nav" rule stated on every primary-nav and Breadcrumb/Pagination page applies per landmark, not per page.- Tabs' roving-tabindex model is self-contained. A Tabs instance inside a larger navigation
composition keeps its own
Tab/ arrow-key contract exactly as documented — composing it alongside Sidebar Nav or Breadcrumb (both plain lists of links, no roving tabindex) doesn't change either model; they simply sit at different points in the page's natural tab order. - Sheet-based mobile menus (Navbar's built-in one, or a caller-composed Sidebar Nav drawer) keep their own focus-trap and restoration contract unchanged — inherited from Sheet, not something this pattern adds to or modifies.
- Contrast. Every token pairing cited in Tokens used already meets WCAG AA on its own primitive's page; composing primitives together changes no color pairing.
Related Components / Patterns
- Navbar — global, top-of-page navigation.
- Sidebar Nav — app-shell navigation on wider viewports.
- Bottom Nav — the mobile-only counterpart.
- Breadcrumb — orientation within a hierarchy.
- Tabs — views on the same page.
- Pagination — moving through a result set.