navigation — chrome that recedes, never disappears
Stage: Phase 6 — Build - Reads: design/SYSTEM.md, design/SITEMAP.md, design/DIRECTION.md - Writes: components/layout/header.tsx (+ mobile-menu.tsx when extracted)
Standard
Navigation is judged by how little you notice it until you need it — then it must be instant, obvious, and keyboard-perfect. First-grade means:
- Sticky with intent: hides on scroll-down, returns on scroll-up. Never an always-fixed bar eating 64-80px of every viewport.
- The mobile menu is a designed moment — typography, choreography, one extra beat of personality — not a shadcn Sheet with a link list dropped in.
- Skip-link, aria-current, focus-visible, Escape handling: all present, all verified by an actual keyboard walkthrough.
- <=7 top-level destinations. More is an information-architecture problem — push back to SITEMAP.md, or go mega-menu deliberately.
- On a multi-section or multi-world page the incoming section's brand color doubles as wayfinding (
award-canon: The Masked Cut — section color as a location signal, owned by page-transitions); the nav's active/location state mirrors it, but never by color alone — always keep the aria-current underline, dot, or weight shift.
- Where SITEMAP.md holds an index or archive, offer Archive-as-Toy browse affordances (
award-canon): filter by unexpected axes (mood, colour, medium — Frans Hals, SOTD 2018 — not just date) and a "surprise me," layered over a working, crawlable, filterable list, never replacing it.
Process
- Read SITEMAP.md for the destination list; the header CTA mirrors the site's #1 conversion goal.
- Pick a variant below per DIRECTION.md. Bar height 56-72px desktop, 56-64px mobile.
- Build the scroll behavior (rules below). The header is a
"use client" leaf — the surrounding layout stays server.
- Build the mobile menu as its own moment (rules below).
- Keyboard pass from a fresh page load: skip-link first, then logo, links in order, CTA, hamburger. Escape closes anything open.
Variants
slim-bar — logo left, links right, one CTA. Default for SaaS/product/local business. The CTA is the only filled element in the bar.
centered-logo — links split around a centered logo. Editorial, fashion, boutique. Requires an explicit balance decision: which side carries the extra link or the CTA.
floating-pill — detached rounded bar inset 12-24px from the viewport top, solid surface from SYSTEM tokens. Modern product/portfolio directions. Backdrop blur only if SYSTEM.md §depth explicitly designed glass — never as a default.
mega-menu — grouped panels under 2-3 trigger items. Only when SITEMAP.md holds 12+ real destinations (docs, multi-product). Panels open on click (hover-intent as enhancement), close on Escape and focus-out, arrow keys move within a panel.
Scroll behavior
- Track direction with an 8px threshold (ignores trackpad jitter); hide only after 96px of scroll depth. Plain React, no library:
"use client";
const [hidden, setHidden] = useState(false);
useEffect(() => {
let last = window.scrollY;
const => {
const y = window.scrollY;
if (Math.abs(y - last) < 8) return;
setHidden(y > last && y > 96);
last = y;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
- Apply as
translateY(-100%) with a 200-250ms transition from SYSTEM's easing family — animate transform only, never top.
- Force-visible when: the mobile menu is open, focus is inside the header (focus-within), or
prefers-reduced-motion — then it is plain sticky, no hide.
- Scrolled state at y > 16px: only then add border/shadow/surface tint. At page top the bar sits flush with the hero; chrome appears once content slides beneath it.
Mobile menu — a designed moment
- Full-screen or near-full panel; links at display scale — the one place nav type gets big: 2-3.5rem via clamp().
- Choreography: panel 300-450ms, links stagger 40-80ms apart, 400-700ms total. Exit via
AnimatePresence (import from "motion/react", file marked "use client"). Reduced motion: instant open/close, no stagger.
- One extra beat: a contact/social row, a surface color shift, or the signature motif — something that makes it THIS site's menu.
- Mechanics:
<button> trigger (44px target) with aria-expanded + aria-controls; focus moves into the panel on open and returns to the trigger on close; focus trapped inside; Escape closes; body scroll locked while open. Build on a restyled shadcn dialog primitive or hand-roll — either way all six behaviors are verified, not assumed.
Active link + states
- Current page:
aria-current="page" (derive via usePathname) plus a non-color-only indicator — underline, dot, or weight shift. One-pagers: section highlight via IntersectionObserver.
- Hover: 150-250ms, ONE move (underline draw, color, or slight shift) — the same move on every nav link, from the
ultraweb:micro-interactions vocabulary.
- focus-visible: palette-token ring on every interactive element;
outline-none without a replacement is a defect.
Skip-link (mandatory)
First focusable element on the page, before the header:
<a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:z-50">Skip to content</a>
Pair with <main id="main" tabIndex={-1}> so the jump actually moves focus. Style the visible state deliberately — it is part of the design, not a legal fig leaf.
Anti-patterns
- No skip-link;
focus:outline-none with no focus-visible replacement.
hidden md:flex on the link list with no mobile menu shipped — mobile users get nothing.
<div onClick hamburger — it is a <button>, always.
backdrop-blur on the bar by default — glass is a depth decision, not a nav reflex.
- Hide-on-scroll with no threshold — the bar flickers on rubber-band scroll.
z-[9999] — layering was never designed; use SYSTEM's z-scale.
- Menu open but body still scrolls behind it; focus escaping the open panel.
- 10+ flat links crammed in the bar; hover-only dropdowns keyboard users cannot open.
Worked example — Tidepool, port-logistics analytics site header
Moved to references/example.md — read only when this build's case is genuinely ambiguous; the sections above are the decision material.
Composes with
Moved to references/composes.md — the handoff map; load it when orchestrating this skill against its neighbors.
1---2name: navigation3description: Design and build the site header and menus for an ultraweb site — four named variants (slim-bar, centered-logo, floating-pill, mega-menu), sticky hide-on-scroll-down/show-on-up behavior, a mobile menu designed as a moment rather than a bare drawer, active-link indication, a mandatory skip-link, and complete keyboard support. Invoke during the Build phase for any header or menu work — trigger phrases — "navbar", "header", "navigation", "menu", "mobile nav", "hamburger", "sticky header", "mega menu".4---56# navigation — chrome that recedes, never disappears78**Stage:** Phase 6 — Build - **Reads:** design/SYSTEM.md, design/SITEMAP.md, design/DIRECTION.md - **Writes:** components/layout/header.tsx (+ mobile-menu.tsx when extracted)910## Standard1112Navigation is judged by how little you notice it until you need it — then it must be instant, obvious, and keyboard-perfect. First-grade means:1314- Sticky with intent: hides on scroll-down, returns on scroll-up. Never an always-fixed bar eating 64-80px of every viewport.15- The mobile menu is a designed moment — typography, choreography, one extra beat of personality — not a shadcn Sheet with a link list dropped in.16- Skip-link, aria-current, focus-visible, Escape handling: all present, all verified by an actual keyboard walkthrough.17- <=7 top-level destinations. More is an information-architecture problem — push back to SITEMAP.md, or go mega-menu deliberately.18- On a multi-section or multi-world page the incoming section's brand color doubles as wayfinding (`award-canon`: The Masked Cut — section color as a location signal, owned by `page-transitions`); the nav's active/location state mirrors it, but never by color alone — always keep the aria-current underline, dot, or weight shift.19- Where SITEMAP.md holds an index or archive, offer Archive-as-Toy browse affordances (`award-canon`): filter by unexpected axes (mood, colour, medium — Frans Hals, SOTD 2018 — not just date) and a "surprise me," layered over a working, crawlable, filterable list, never replacing it.2021## Process22231. Read SITEMAP.md for the destination list; the header CTA mirrors the site's #1 conversion goal.242. Pick a variant below per DIRECTION.md. Bar height 56-72px desktop, 56-64px mobile.253. Build the scroll behavior (rules below). The header is a `"use client"` leaf — the surrounding layout stays server.264. Build the mobile menu as its own moment (rules below).275. Keyboard pass from a fresh page load: skip-link first, then logo, links in order, CTA, hamburger. Escape closes anything open.2829## Variants3031**slim-bar** — logo left, links right, one CTA. Default for SaaS/product/local business. The CTA is the only filled element in the bar.3233**centered-logo** — links split around a centered logo. Editorial, fashion, boutique. Requires an explicit balance decision: which side carries the extra link or the CTA.3435**floating-pill** — detached rounded bar inset 12-24px from the viewport top, solid surface from SYSTEM tokens. Modern product/portfolio directions. Backdrop blur only if SYSTEM.md §depth explicitly designed glass — never as a default.3637**mega-menu** — grouped panels under 2-3 trigger items. Only when SITEMAP.md holds 12+ real destinations (docs, multi-product). Panels open on click (hover-intent as enhancement), close on Escape and focus-out, arrow keys move within a panel.3839## Scroll behavior4041- Track direction with an 8px threshold (ignores trackpad jitter); hide only after 96px of scroll depth. Plain React, no library:4243```tsx44"use client";45const [hidden, setHidden] = useState(false);46useEffect(() => {47 let last = window.scrollY;48 const onScroll = () => {49 const y = window.scrollY;50 if (Math.abs(y - last) < 8) return;51 setHidden(y > last && y > 96);52 last = y;53 };54 window.addEventListener("scroll", onScroll, { passive: true });55 return () => window.removeEventListener("scroll", onScroll);56}, []);57```5859- Apply as `translateY(-100%)` with a 200-250ms transition from SYSTEM's easing family — animate transform only, never `top`.60- Force-visible when: the mobile menu is open, focus is inside the header (focus-within), or `prefers-reduced-motion` — then it is plain sticky, no hide.61- Scrolled state at y > 16px: only then add border/shadow/surface tint. At page top the bar sits flush with the hero; chrome appears once content slides beneath it.6263## Mobile menu — a designed moment6465- Full-screen or near-full panel; links at display scale — the one place nav type gets big: 2-3.5rem via clamp().66- Choreography: panel 300-450ms, links stagger 40-80ms apart, 400-700ms total. Exit via `AnimatePresence` (import from `"motion/react"`, file marked `"use client"`). Reduced motion: instant open/close, no stagger.67- One extra beat: a contact/social row, a surface color shift, or the signature motif — something that makes it THIS site's menu.68- Mechanics: `<button>` trigger (44px target) with `aria-expanded` + `aria-controls`; focus moves into the panel on open and returns to the trigger on close; focus trapped inside; Escape closes; body scroll locked while open. Build on a restyled shadcn dialog primitive or hand-roll — either way all six behaviors are verified, not assumed.6970## Active link + states7172- Current page: `aria-current="page"` (derive via usePathname) plus a non-color-only indicator — underline, dot, or weight shift. One-pagers: section highlight via IntersectionObserver.73- Hover: 150-250ms, ONE move (underline draw, color, or slight shift) — the same move on every nav link, from the `ultraweb:micro-interactions` vocabulary.74- focus-visible: palette-token ring on every interactive element; `outline-none` without a replacement is a defect.7576## Skip-link (mandatory)7778First focusable element on the page, before the header:7980```tsx81<a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:z-50">Skip to content</a>82```8384Pair with `<main id="main" tabIndex={-1}>` so the jump actually moves focus. Style the visible state deliberately — it is part of the design, not a legal fig leaf.8586## Anti-patterns8788- No skip-link; `focus:outline-none` with no focus-visible replacement.89- `hidden md:flex` on the link list with no mobile menu shipped — mobile users get nothing.90- `<div onClick` hamburger — it is a `<button>`, always.91- `backdrop-blur` on the bar by default — glass is a depth decision, not a nav reflex.92- Hide-on-scroll with no threshold — the bar flickers on rubber-band scroll.93- `z-[9999]` — layering was never designed; use SYSTEM's z-scale.94- Menu open but body still scrolls behind it; focus escaping the open panel.95- 10+ flat links crammed in the bar; hover-only dropdowns keyboard users cannot open.9697## Worked example — Tidepool, port-logistics analytics site header9899Moved to `references/example.md` — read only when this build's case is genuinely ambiguous; the sections above are the decision material.100101## Composes with102103Moved to `references/composes.md` — the handoff map; load it when orchestrating this skill against its neighbors.