Nav Shell
A full-page top-navigation layout: a sticky header bar with brand, inline navigation, and actions above a scrollable content region. On mobile the inline nav collapses into a hamburger-triggered drawer. Use for marketing sites and top-nav applications.
Structure Diagram
NavShellBrandNavShellNavItem (active)
NavShellNavItem
NavShellActions
NavShellMain (scrollable)
The inline NavShellNav is hidden below the md breakpoint, where NavShellMobileNav renders a hamburger that opens a drawer with the same links. Resize the browser to see the switch.
A header with no nav
Some applications have one destination. A staff tool where every deeper page opens with a back arrow has nowhere for a nav menu to go, and a candidate-facing page has a company to name and nothing to browse. Both still want the same chrome: a mark, a few controls, a rule under them. That is NavShellBrandStrip, and it sits in the same slot as NavShellHeader inside the same NavShell.
NavShellMain
The specimen is the strip alone, not a whole shell: this page already has a <main>, and a document gets one. Switch the direction toggle to RTL and the mark moves to the other edge — the flush side is pe-10 and no start padding, so it follows the reading direction rather than the screen.
What the strip is, and what it is not
It is a banner. A <header> holding the site mark and the site-wide controls, which is what banner means. HTML-AAM maps <header> to that role unless it sits inside an article, aside, main, nav or section; NavShell is a <div>, so the mapping holds here exactly as it does for NavShellHeader.
It is not a navigation landmark. There is no <nav> and no aria-label="Primary", because a navigation landmark promises a list of destinations and this strip has a mark and a sign-out button. That is not left as advice: NavShellNav and NavShellMobileNav throw inside the strip, and the message names NavShellHeader as the part to use instead. A bare NavShellNavItem in the actions is fine — a link claims no landmark on its own.
It is not a toolbar. The actions are an ordinary cluster, so each control keeps its own tab stop. A role="toolbar" would collapse a theme toggle, a link and a sign-out form into one stop behind a roving tabIndex, which is a grouping none of them asked for.
It does not retire the skip link. WCAG 2.4.1 Bypass Blocks is about blocks that repeat, and the actions repeat on every page. With no nav the block is shorter, not absent. Render SkipLink as the first child of NavShell; NavShellMain is already the target it looks for.
Why a part and not a prop
NavShellHeader renders two elements: the <header> that carries the sticky band, and an inner mx-auto max-w-6xl box so brand, nav and actions share one centred measure. The strip wants the opposite of that inner box — nothing reaches the viewport edge from inside a centred column — so it is one element rather than two.
A variant or a noNav flag would therefore have to delete an element, and a prop that deletes an element is two components wearing one name. A separate organism was the other way out, and it would have meant a second copy of NavShellBrand, NavShellActions and NavShellMain for the sake of one box. The shell is a header over a main; the nav was only ever one part of the header, and now there are two header parts to choose between.
Where it differs from NavShellHeader
Four deliberate differences, and none of them is configurable — pass className if you want otherwise. It does not stick. A sticky bar earns its height by keeping destinations in reach, and this one has no destinations, so it scrolls away with the page. It is opaque (bg-background, not bg-card/90 backdrop-blur), because nothing slides under a bar that does not stay. It is four pixels taller — h-15 against h-14 — because its content sits directly against its own box with no padded column to give it room. It pads its end only, so the mark is flush and the actions clear the edge.
Everything else is shared. NavShellBrand, NavShellActions and NavShellMain are the same parts inside either header, and swapping one header for the other changes no other line.
NavShell Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes to apply to the shell container. |
The same className passthrough applies to NavShellHeader, NavShellBrand, NavShellNav, NavShellActions, and NavShellMain.
NavShellBrandStrip Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the <header> itself. There is no inner column, so this is the only box there is: pass ps-4 to give the mark a start inset, or h-14 to match NavShellHeader. |
children | React.ReactNode | — | Typically NavShellBrand and NavShellActions. NavShellNav and NavShellMobileNav throw here — the strip is a banner that holds no navigation, and that claim is enforced rather than documented. |
NavShellMain Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | "main-content" | The fragment SkipLink jumps to. Matches SkipLink's own default, so the pair needs no wiring. Override both together for a page with a different target. |
tabIndex | number | -1 | Makes the skip-link target focusable without adding a tab stop. Without it the browser scrolls to the anchor and leaves focus in the header the reader just asked to bypass. |
Both are ordinary props with defaults, so a page with a different target says so: <NavShellMain id="report"> with <SkipLink href="#report" />. The outline-none is deliberate — focus only ever arrives here programmatically, and a ring around the whole page tells a reader who just pressed the link nothing they do not know.
NavShellNavItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ComponentType<{ className?: string }> | — | Optional icon component rendered before the label. |
active | boolean | false | Whether this item represents the current page. |
href | string | — | The link destination for the navigation item. |
NavShellMobileNav Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Navigation" | Accessible title shown at the top of the mobile drawer. |
triggerLabel | string | "Open navigation menu" | Accessible label for the hamburger trigger button. |