Nav
Chrome is where the material world is either carried through the whole page or quietly dropped.
Load ../gmira/references/DOCTRINE.md first. Install anything from a registry via
gmira-arsenal.
The premise
A page can have a committed direction in the hero and a completely undecided header, and almost every generated site does. The header is the first thing built, before the direction exists, and it is never revisited. So the specificity test applies hardest here:
Could an unrelated product use this header unchanged? If yes, nothing was decided. The header is where a 1970s parts catalogue becomes hairline rules and part numbers, or fails to.
Refuse these four
| Refused | Why it is a tell | Instead |
|---|---|---|
The floating pill nav: fixed top-6 rounded-full border backdrop-blur-md bg-white/60 |
It is on every AI-assembled site shipped since 2024. Glass and blur as decoration rather than as a specific effect. | A header that touches the edges and is separated by a rule, or no header chrome at all above the fold. Declare elevation once: a border or a shadow, not both. |
| A hamburger on desktop | It hides a five-item list behind a click to buy back space nobody needed. | Show the links. If they do not fit, there are too many links, which is a content problem. |
| A mega-menu for six destinations | Chrome standing in for content. | Six links. A mega-menu is earned by inventory depth (a real catalogue with categories and counts), never by ambition. |
@componentry/magnetic-dock on anything that is not a desktop-app metaphor |
A macOS dock on a store is cosplay. It borrows a signal from an operating system the product is not. | Ordinary navigation, styled from the brief's own world. |
Step 1: the shape comes from the mode
Mode is per surface. A developer tool's landing page is Persuade even though the product is Operate.
| Mode | The nav's job | Concrete shape |
|---|---|---|
| Persuade | Get out of the way, then convert | 3 to 5 destinations, one action, no search, no account menu. The action is the only filled control on the page's chrome. |
| Operate | Carry persistent state and get to anything fast | Current context always visible (workspace, project, filter count), search or command palette, account state, unsaved and error indicators. Effect budget near zero. |
| Read | Say where you are and how far there is to go | Section position, a table of contents or progress rail, previous and next at the end. aria-current on the active item is not decoration here, it is the whole feature. |
| Experience | Be part of the work, or disappear | Either the nav is a composed element of the piece, or it hides on scroll down and returns on scroll up. Pick one and commit. |
INCORRECT a landing page header with Product, Solutions, Resources, Company, Pricing,
Docs, Blog, a search icon, a theme toggle, Log in, and Get started.
CORRECT Persuade: three destinations that a buyer actually needs, plus one action.
Everything else lives in the footer, where a visitor who is looking will look.
Step 2: the line system
The most transferable structural device for chrome is a rule token plus two utilities. It is
shipped as a CSS-only registry item, @ncdai/style, which writes no component files at all.
--line: color-mix(in oklab, var(--border) 64%, var(--background));
@utility screen-line-top {
@apply relative;
&:before { content: ""; @apply absolute top-0 left-[-100vw] -z-1 h-px w-[200vw] bg-line; }
}
@utility screen-line-bottom {
@apply relative;
&:after { content: ""; @apply absolute bottom-0 left-[-100vw] -z-1 h-px w-[200vw] bg-line; }
}
@utility diagonal-stripes {
@apply [--pattern-foreground:var(--color-line)]/56;
@apply bg-[repeating-linear-gradient(315deg,var(--pattern-foreground)_0,var(--pattern-foreground)_1px,transparent_0,transparent_50%)] bg-size-[10px_10px];
}
A 200vw pseudo-element offset -100vw at -z-1 makes every rule run edge to edge regardless of
container width. The line color is deliberately softer than --border, and in dark mode the
override is left out on purpose so the same color-mix formula re-solves against the dark
background.
Applied, the header stops being a floating object and becomes part of a ruled sheet:
<header className="screen-line-bottom sticky top-0 z-40 h-(--header-height) border-x border-line bg-background/95">
Two rules that come with it:
- Derive the anchor offset from the variables that create it. If the header is
--header-heightand the section rhythm is--separator-height, thenscroll-mt-[calc(var(--header-height)+var(--separator-height))]stays correct when either changes. Hardcodingscroll-mt-24is a bug waiting for a redesign. - Apply the rules with selectors, not props.
nth-[3n+1]:screen-line-topdraws the row rule only on the first item of each row at each breakpoint.has-data-[slot=panel-description]:*:data-[slot=panel-title]:screen-line-bottomis the design rule "the title gets its own rule only when there is a description below it", written as a selector.
If the brief's world is not ruled paper, this is still the pattern: pick one structural token (a rule, a stripe, a notch, a registration mark), ship it as a utility, and apply it everywhere. Chrome feels authored when one small idea is applied consistently, not when each part is styled.
Step 3: the desktop header
<a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:top-3 focus:left-3
focus:z-50 focus:rounded-md focus:bg-background focus:px-3 focus:py-2
focus:outline-2 focus:outline-offset-2 focus:outline-ring">
Skip to content
</a>
<header className="screen-line-bottom sticky top-0 z-40 flex h-(--header-height) items-center gap-6">
<nav aria-label="Primary" className="flex items-center gap-1">
{items.map((item) => (
<a key={item.href} href={item.href}
aria-current={item.href === pathname ? "page" : undefined}
className="rounded-md px-3 py-2 text-sm text-muted-foreground transition-[color]
hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-ring aria-[current=page]:text-foreground">
{item.label}
</a>
))}
</nav>
</header>
Style from the accessible attribute. aria-[current=page]:text-foreground means the
accessibility state and the appearance cannot diverge, because there is only one source. A separate
isActive boolean driving a class always drifts.
@ncdai/line-nav is the same idea taken further and is worth reading even if you do not install it:
the anchor is literally h-px, the marker is a <span> whose width animates 24 to 40 with a
spring, the hit target comes from an ::after at p-3.5 (28px) that changes no layout, and active
styling is group-aria-[current=page]:. initial={false} keeps it from animating on first paint.
Dependency: motion.
Sticky header with content behind it needs one of: an opaque background, backdrop-filter with a
tested contrast, or hide-on-scroll-down. Half-transparent with body text passing under it fails
contrast at some scroll position, and it fails silently.
Step 4: mobile is not a full-screen overlay by default
The full-screen overlay is the reflex, and it is the heaviest possible answer: it needs a focus
trap, inert on the rest of the document, a scroll lock, an escape path, and a focus return. Most
sites need none of that.
| Destinations | Correct pattern | Focus handling |
|---|---|---|
| 3 to 6, Persuade | An inline disclosure that pushes content down, or an anchored dropdown | No trap. aria-expanded and aria-controls on the trigger, Escape closes, focus stays where it was. |
| 3 to 5, Operate | A persistent bottom bar with aria-current |
None needed, it is always visible |
| 6 to 12, mixed | A bottom sheet that stops at about 60vh so the page stays visible behind it | Modal: trap, inert, Escape, focus return |
| The nav is the content (Experience, a menu with imagery, a full catalogue) | Full-screen overlay, and now it is earned | Modal, plus the overlay itself has to be a designed composition |
INCORRECT const [open, setOpen] = useState(false)
{open && <div className="fixed inset-0 z-50 bg-background">...links...</div>}
// no role, no trap, no inert, no Escape, no focus return, and the background scrolls
CORRECT <dialog ref={ref} className="m-0 h-svh max-h-none w-full max-w-none bg-background
backdrop:bg-black/40">
// opened with ref.current.showModal()
// native <dialog> gives the focus trap, top-layer stacking, inert background,
// and Escape for free. Add only: focus return on close, and the scroll lock.
Scroll lock without layout shift:
html { scrollbar-gutter: stable; }
body:has(dialog[open]) { overflow: hidden; }
Focus return, which showModal does not do on its own:
const trigger = useRef<HTMLButtonElement>(null);
const close = () => { dialogRef.current?.close(); trigger.current?.focus(); };
A keyboard trap is correct inside a modal dialog and a bug anywhere else. If the panel does not block the rest of the page, do not trap; let Tab walk out of it.
Step 5: command menu, earned or cosplay
@componentry/command-menu is a macOS Spotlight-style palette: cmdk plus framer-motion plus
lucide-react, with groups, an empty state, and a configurable shortcut key. cmdk is small. The
cost is not bundle size, it is the signal.
| Earned | Cosplay |
|---|---|
| Operate mode with more destinations than a nav can hold: an admin, a dashboard, a docs site with 200 pages, an inventory with real filters | A five-page marketing site where the palette lists the same five links as the header |
| The palette does something the nav cannot: run actions, jump to a record by name, switch workspace, toggle a setting | The palette is search over three headings, and search already exists |
| The audience is developers and the surface is the tool | The audience is developers and the surface is the pricing page. A developer-tool signal on a marketing page is cosplay. |
Same test kills @componentry/mac-keyboard on a course sales page and keeps it on a page that
teaches shortcuts.
If it is earned, three requirements: it must be reachable without the keyboard (a visible button, not only Cmd+K), the shortcut must be shown on the button, and it must have a real empty state that says what to try. A palette that returns "No results." to every typo is a demo.
Step 6: the footer is a surface
The footer is the last thing a visitor reads before deciding whether the business is real. Four columns of plausible-looking links is chrome standing in for content, and it usually contains invented destinations, which fails doctrine G7 outright.
Rules:
- Every link goes somewhere that exists. No
href="#". No social icons for accounts nobody opened. - No invented metrics, testimonials, logos, or client names, including in the footer's "trusted by" strip.
- The footer is a legitimate place for the page's second and last effect band, but only when it
costs no new library.
@componentry/closing-plasmais the raw-WebGL1 sibling ofsilk-auroraandwebgl-liquid, so a hero from that family plus this footer bookends the page for the price ofclsxandtailwind-merge. Props worth setting:themeMode,turbulence,sparkle,vignette, and thedarkColorA/B/CpluslightColorA/B/Cpalettes. - The line system continues here. A footer that drops the rule grammar reads as a different site.
Content model per vertical
| Vertical | What actually belongs |
|---|---|
| Car shop | Physical address, opening hours per day, phone that dials, dealer registration number, the real inventory categories with counts, finance and APR representative example, part-exchange and warranty terms |
| E-commerce | Returns window and who pays postage, delivery times per region, size guide, payment methods actually accepted, legal entity and VAT number, a contact route with a response time |
| AI school | Cohort start dates, syllabus link, hours per week, admissions contact, refund and deferral policy, named instructors, prerequisites |
| GTM / UGC school | Earnings disclaimer, terms, refund window with the actual number of days, support email, only the social accounts that exist and post |
Legal and policy links are not filler. On a commerce or education surface they are the highest-read links in the footer and they belong in one clearly labelled group, not scattered to balance columns.
Small figures (seats remaining, cohort dates, stock counts) can go to
@componentry/split-flap-display: zero dependencies, inline @keyframes, mechanical rather than
SaaS-generic. Only for real numbers.
Accessibility, non-negotiable
| Requirement | Implementation | How to verify |
|---|---|---|
| Skip link | First focusable element in the DOM, visually hidden until focused, targets the <main id="main"> |
Load, press Tab once, it must be visible and land in main |
| Focus visible | :focus-visible outline with outline-offset, on every interactive element including the logo. Never outline: none without a replacement |
Tab the whole header and footer; no invisible stops |
| Current position | aria-current="page" on the active nav item, aria-current="step" in a flow. Style from it |
Inspect the active link; the attribute is present and drives the class |
| Escape closes | Every overlay, menu, and palette. Native <dialog> and Radix give it; a hand-rolled div does not |
Open, press Escape, it closes |
| Focus returns | On close, focus goes back to the element that opened it | Open with the keyboard, close with Escape, next Tab continues from the trigger |
| Trap is scoped | Trapped inside modal dialogs only. Non-modal disclosures let Tab leave | Tab past the last item of a dropdown; focus should exit |
| Landmarks | One <header>, <nav aria-label="Primary">, <main>, <footer>. A second nav gets its own label |
Screen reader landmark list has no duplicates |
| Names | Icon-only buttons carry aria-label. The theme toggle says which theme it switches to |
Every control has an accessible name |
| Hit target | 44px minimum on touch, added with a pseudo-element so layout does not change: after:absolute after:size-full after:p-3.5 |
Measure on a 390px viewport |
| Reduced motion | Hide-on-scroll headers, marker springs, and menu transitions all off | Toggle the media query |
The six states apply to nav controls like anything else: hover, focus, disabled, loading, error, empty. A search field in a header needs a loading state and an empty state, and both get skipped.
Checks before this skill is done
- The header traces to the direction contract. An unrelated product could not use it unchanged.
- Zero of the four refused patterns, or one is present and logged with the brief words that earned it
- Link count and the single action match the surface's mode
- One structural token (rule, stripe, notch) is defined once and applied by selector across header, sections, and footer
- Anchor scroll offset derives from the header and rhythm variables, not a hardcoded value
- Skip link is the first focusable element and becomes visible on focus
-
aria-currentis present on the active item and is what drives its styling - Every overlay: Escape closes, focus returns to the trigger, background is
inertonly when modal - Mobile menu is not a full-screen overlay unless the nav is the content
- Scroll lock does not shift layout (
scrollbar-gutter: stable) - Command palette exists only if it does something the nav cannot, and is reachable without a keyboard
- Sticky header contrast measured at a scroll position where body text is behind it
- Every footer link resolves. Zero invented links, metrics, logos, or social accounts.
- Footer carries the vertical's real content model, not four balanced columns
- Second effect band, if any, adds no new dependency