# Navigation component group — design Date: 2026-08-07 Status: approved, not yet implemented ## Problem The navigation group has nine components. Five of them — Nav, MegaMenu, Scrollspy, Pagination, Stepper — are byte-identical scaffold stubs that differ only in one CSS class name. Each renders `{#each items}{label}{/each}` and nothing else. MegaMenu has no panel, Scrollspy never observes scroll, Stepper has no steps or progress, Pagination has no pages. Of the remaining four, Breadcrumb is genuinely built (323 lines, own style block). Navbar and Sidebar render but have no keyboard handling and keep their styles in `ui.css`. Tabs works but is off-pattern in three ways: it is the only component in the library using Tailwind utility classes rather than `wrn-*` + a local style block, it has no `outputs` block and fires raw `CustomEvent`s via `dispatchEvent`, and it sets a roving `tabindex` with no `@keydown` handler at all — which is worse than having no keyboard support, because the roving tabindex makes every inactive tab unreachable by Tab while arrows do nothing. ## Scope All nine components, delivered in three phases. ## Decisions ### Styling Every navigation component gets a local `style {}` block using `wrn-*` classes. This matches DataTable, Toaster, Modal, Drawer, ContextMenu and Breadcrumb — the 22 of 108 components that carry local styles are exactly the recently built ones. Navbar and Sidebar styles move out of `ui.css` as part of this work. ### Dropdowns: Nav vs MegaMenu Both get dropdowns, of deliberately different kinds. - **Nav** gets multi-level cascading submenus. It is the link bar, so nested submenus are its job. - **MegaMenu** gets a single-level rich panel: columns of grouped links with headings, descriptions and icons. This is deliberate, not a shortcut. A mega menu exists to show breadth flat so everything is one click away; nesting inside the panel buries content behind hover-within-hover and is close to unusable by keyboard and touch. Neither reuses the existing Dropdown component: Dropdown is click-triggered and has no nesting support. **Depth limit.** There is no recursive-component precedent in this library, so multi-level means fixed depth via nested `{#each}` loops — the pattern Navbar (4 loops) and Sidebar (2 loops) already use. Depth is **3 levels**. Unlimited nesting would require proving out component self-reference, which is out of scope here. ### Roving focus lives in the runtime Arrow-key roving focus is identical logic for Tabs, Nav, MegaMenu, Sidebar and Stepper. It goes in the reactive runtime as a declarative attribute rather than five near-identical client functions: - `data-wrn-roving="horizontal|vertical|both"` on the container - `[data-wrn-roving-item]` on each focusable child The runtime owns arrow keys, Home/End, wrap-around, skip-disabled, and maintenance of the roving `tabindex`. Components declare intent only. This follows the modal-dialog focus work already in the runtime, and for the same reason: a client function cannot hold focus state across callbacks, because state written after the function returns is dropped. ### Scrollspy needs runtime support too `data-wrn-scrollspy` backed by `IntersectionObserver`. It cannot be a client function — the observer callback fires long after the function returns, and that state write would be lost. ### Runtime budget Roving focus is roughly 3–4k, scrollspy roughly 1.5k. The runtime is at 167k against the 175k ceiling raised on 2026-08-07. This fits, but leaves little room. The group after this one forces the decision about splitting the runtime into loadable chunks so pages pay only for behaviour they use. ### Tabs URL mode `mode="client" | "url"`. URL mode uses a query parameter (`?tab=value`) driven by `history.pushState`, so content swaps with no page load and the back button works. Query parameter rather than hash: it is shareable, survives reload, and does not collide with in-page anchors or with Scrollspy, which wants the hash. Tabs also gets a panel transition on change. ### Sidebar composes Drawer Sidebar does not reimplement off-canvas behaviour. Desktop renders a static rail; mobile renders inside Drawer. This inherits Drawer's focus trap and scroll lock rather than duplicating them. ### Cross-cutting requirements - Icons on every menu item, tab, and step - Dropdown arrows that animate on open - Responsive behaviour per component: Nav collapses to a toggle, MegaMenu stacks its panel, Tabs becomes a scrollable strip, Stepper goes vertical, Pagination goes compact - Full ARIA Authoring Practices patterns for each role, including correct `aria-current` / `aria-selected` / `aria-expanded` ## Components | Component | Work | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Nav | Build. Flat or multi-level (3 deep), horizontal/vertical, `items[{label,href,value,icon,badge,disabled,items}]`, `aria-current="page"`, roving focus, animated arrows, collapses on mobile. `outputs: select` | | Tabs | Rewrite off Tailwind onto `wrn-*` + local styles. Real `outputs {change, select}` replacing raw `dispatchEvent`. Roving + Home/End. `mode="client"\|"url"`. Panel transition. Fix the `` that renders regardless of active tab | | Pagination | Build standalone. `page`/`pageSize`/`total`, compact + numbered styles, windowed page numbers. `outputs: change, previous, next` | | Stepper | Build. `
    `/`
  1. `, status derived from `active` index, horizontal/vertical, optionally clickable, `aria-current="step"`. Indexed named slots (`data-slot="step-0"`, `step-1`, …) for custom per-step content, falling back to built-in rendering | | MegaMenu | Build. Trigger plus a single-level panel of link columns. Panel marked `data-wrn-anchored` so the existing clamp handles viewport containment. Hover and focus open, Escape closes, click-outside closes | | Scrollspy | Build. Observes section ids, moves `aria-current` to the matching link | | Navbar | Audit. Styles move to a local block, roving focus on the link group, keyboard for the mobile toggle | | Sidebar | Audit and extend. Local styles, composes Drawer for mobile, single items / labelled groups / multi-level (3 deep), vertical roving | | Breadcrumb | Audit only — already the strongest of the nine | Component count stays at 108; all nine files already exist. ## Data flow Props in, outputs out, no global state. Every component takes its data as props and reports interaction through declared `outputs`. ## Error handling `items` arrives as an HTML attribute and is frequently a JSON string, so: - A non-array `items` renders empty rather than throwing - Out-of-range `active` / `page` clamps to bounds ## Testing - Per-component entries in `packages/ui/test/ui.test.ts` covering names, declared outputs and props - Showcase profiles for each so every component gets live demos - Regenerate component reference, catalog, showcase, and the UI visual contract - Each phase ends with `bun run check:production` green ## Phases Each phase ends green, committed and pushed. 1. **Runtime and primitives** — roving-focus runtime, Nav, Pagination, Stepper 2. **Composed** — Tabs (rewrite, URL sync, animation), Sidebar (on Drawer), MegaMenu 3. **Audit and polish** — Navbar, Breadcrumb, Scrollspy, responsive pass, showcase generation ## Risks - **Scrollspy and MegaMenu** are the only two needing new runtime behaviour. MegaMenu leans on the anchored clamp, which could not be verified interactively on 2026-08-07 because the browser pane was degraded (screenshots timing out, `scrollIntoView` inert). Verify the pane works before starting phase 2. - **Tabs is a breaking change.** Replacing raw `CustomEvent`s with declared outputs changes its public contract; anything listening for the old events breaks. 0.8.5 shipped on 2026-08-07, so this needs a migration entry in `packages/cli/src/update.ts`. ## Codebase constraints to respect Hazards this codebase has already hit: - No apostrophes in `.wrn` comments — the brace scanner breaks on them - `/* */` only inside style blocks; `//` is not a CSS comment and silently eats the following rule - No deferred state writes in client functions; state written after the function returns is dropped - Never call a peer function after an application callback — the wrapper flushes the entry-time snapshot - No boolean attributes bound to loop variables - Package components need explicit imports