Navigation menu
Navigation menu navigation Controller
Top product/site nav with optional mega-menu panels — horizontal, not the app-shell sidebar. Triggers use native details.
Layer: L1 surface · Recipe: unset — see docs/agent/pick-a-surface.md. Curriculum: AGENTS.md; pick matrix: docs/agent/pick-a-surface.md; blast radius: CONSUMER_MAP.md.
Copy this
<nav class="navigation-menu" data-navigation-menu aria-label="Product">
<ul class="navigation-menu__list">
<li class="navigation-menu__item"><a class="navigation-menu__link" href="#" aria-current="page">Home</a></li>
<li class="navigation-menu__item">
<details class="navigation-menu__branch">
<summary class="navigation-menu__trigger">Product</summary>
<div class="navigation-menu__panel" data-layout="mega">
<div class="navigation-menu__group">
<p class="navigation-menu__group-title">Build</p>
<a href="#">DSL apps<small>Ship CRUD + workflows</small></a>
<a href="#">Hyperparts<small>Gallery + contracts</small></a>
</div>
<div class="navigation-menu__group">
<p class="navigation-menu__group-title">Operate</p>
<a href="#">Deploy<small>Plan + target</small></a>
<a href="#">Observability<small>Pulse + fitness</small></a>
</div>
</div>
</details>
</li>
<li class="navigation-menu__item">
<details class="navigation-menu__branch">
<summary class="navigation-menu__trigger">Resources</summary>
<div class="navigation-menu__panel">
<div class="navigation-menu__group"><a href="#">Docs</a><a href="#">Changelog</a><a href="#">Community</a></div>
</div>
</details>
</li>
<li class="navigation-menu__item"><a class="navigation-menu__link" href="#">Pricing</a></li>
</ul>
</nav>Server exchange
This Hyperpart has no server exchange — it is presentation or client chrome only. htmx does not issue a request on this part's behalf. If you put an affordance (hx-*) on a control that uses this markup, that action's exchange belongs to the action, not this part. See Swap contract for host-owned envelopes.
Swap contract
Agent-visible HTMX topology (ADR-0054 / decision 0012). exchange envelope = what the response may re-emit relative to the persistent slot (body_only | outer | none | host_owned | document). dual-lock validates part markup only — not this envelope. Stem: stems/morph-safe-hypermedia.md; decision: docs/decisions/0012-swap-identity-contract.md.
No host HTMX exchange on this part — presentation or client chrome only. exchange envelope: n/a.
If a host wraps this markup in hx-*, that host owns the swap contract (sole identity + envelope). Prefer innerMorph / outerMorph for stable slots; replacement for flash; body-only responses under inner swaps.
Envelope response examples
This part has no owned exchange (envelope n/a). If a host adds hx-*, that host’s envelope applies — typically body_only:
<!-- Host wraps this presentation part with hx-* (host owns envelope) -->
<!-- Prefer: hx-swap="innerMorph" hx-target="#panel-body" -->
<!-- Server returns body_only interior for #panel-body -->
<div class="dz-stack">content…</div>
Do not re-own the slot:
<!-- WRONG: server returns the presentation root with a new id every poll -->
<div id="navigation-menu-root" data-dz-region>…</div>
How to use it
Seams
- `[data-navigation-menu]` / `.navigation-menu` scopes exclusive open
- `details.navigation-menu__branch` + `summary.navigation-menu__trigger`
- Submenu affordance: CSS `::after` chevron on trigger (1rem, rotates open)
- pick-a-surface: top product/site go-to → navigation-menu (not menubar / menu / sidebar)
Do / Don't
| Do | Don't |
|---|---|
| Let the controller close siblings on toggle and outside click | Leave multi-open mega panels as bare multi-details |
| CSS/SVG disclosure chevron at ~1rem control scale | Tiny Unicode caret as the only submenu signal |
| top nav with links + optional mega panels (go somewhere) | File/Edit command strip (menubar) or one Actions dropdown (menu) |
Pitfalls
- Native details allow multi-open and ignore outside click — ship navigation-menu.js
- Do not confuse with menubar (app File/Edit) or app-shell sidebar
- Gallery: href=# is product-shaped stand-in; MOCK_HTMX inert-hash handler stops host scroll — do not 'fix' Copy this to void(0)
- Do not reintroduce Unicode ▾ spans or 0.65em carets — match accordion disclosure chrome (stem affordance-disclosure-chrome)
- do not use navigation-menu for local action lists — that is menu
Keyboard / AT
- aria-label on root nav
- Keyboard: Enter/Space toggles summary; Escape dismisses open panels
- Chevron is decorative (CSS); details/summary carry expand semantics
Related parts
DOM contract
What the emitted HTML must satisfy — the table is the required surface; Python under contracts/ is the package-internal dual-lock CI runs (tests/test_contracts.py), not an app route. Standalone HTMX4: implement the API so responses match this markup. Dazzle: the agent emits SSR that already satisfies it. Do not invent attrs outside these tables. For request/response wiring see Server exchange.
contracts/navigation_menu.py
Required in the DOM: root [data-navigation-menu] (part navigation-menu). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).
| Node | Attr | Constraint |
|---|---|---|
[data-navigation-menu] | data-navigation-menu | present (any value) |
Module source
Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.
"""HYPERPART: navigation-menu — product nav exclusive-open root contract."""
from contracts._kit import DomContract, Node, Present
DOM_CONTRACT = DomContract(
part="navigation-menu",
root="[data-dz-navigation-menu]",
nodes=(
Node(
"[data-dz-navigation-menu]",
attrs={"data-dz-navigation-menu": Present()},
),
),
)
__all__ = ["DOM_CONTRACT"]
Notes
docs/agent/pick-a-surface.md › Menus / panels / chrome strips. Open intent: exclusive + outside/Escape dismiss. Disclosure chevron is CSS on the trigger (not Unicode). Mega layout via data-layout=mega. navigation-menu.js; probes exclusive_open + dismiss_outside. shadcn parity (HMC-039).Source files
One logical Hyperpart, 4 code items (CSS layered, JS bundled). Bound by HYPERPART: navigation-menu — python tools/hyperpart.py navigation-menu lists them.
site/registry.py · contracts/navigation_menu.py · components/navigation-menu.css:1 · controllers/dz-navigation-menu.js