Navigation Menu
Site-level navigation with dropdown content panels.
Default
<nav class="nav-menu" aria-label="Main">
<ul class="nav-menu-list">
<li class="nav-menu-item"><a class="nav-menu-link" href="#">Home</a></li>
<li class="nav-menu-item">
<button class="nav-menu-trigger" popovertarget="dd">Products</button>
<div class="nav-menu-content" id="dd" popover>...</div>
</li>
</ul>
</nav>
With dropdowns
Triggers open anchor-positioned content panels. Chevron rotates when open.
<nav class="nav-menu" aria-label="Site navigation">
<ul class="nav-menu-list">
<li class="nav-menu-item">
<button class="nav-menu-trigger" popovertarget="nav-products">
Products
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="nav-menu-content" id="nav-products" popover>
<a class="nav-menu-content-link" href="#">
<strong>Analytics</strong>
<p>View your dashboard and metrics</p>
</a>
<a class="nav-menu-content-link" href="#">...</a>
</div>
</li>
<li class="nav-menu-item">
<a class="nav-menu-link" href="#">Pricing</a>
</li>
</ul>
</nav>
CSS view file
Styles for the navigation-menu component. Uses design tokens for colors, spacing, and radius.
@layer components {
.nav-menu-list { display: flex; align-items: center; gap: 0.25rem; list-style: none; margin: 0; padding: 0; }
.nav-menu-link, .nav-menu-trigger {
display: inline-flex; align-items: center; gap: 0.25rem;
padding: 0.5rem 0.75rem; border: none; border-radius: var(--radius-md);
background: transparent; color: var(--foreground); font-size: 0.875rem; font-weight: 500;
font-family: inherit; text-decoration: none; cursor: pointer;
transition: background-color 150ms ease, color 150ms ease;
outline: none;
&:hover { background-color: var(--accent); color: var(--accent-foreground); }
&:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
& svg { width: 0.875rem; height: 0.875rem; color: var(--muted-foreground); transition: transform 200ms ease; }
}
/* Rotate chevron when popover is open */
.nav-menu-trigger:has(+ .nav-menu-content:popover-open) svg {
transform: rotate(180deg);
}
.nav-menu-content {
position: fixed; inset: auto;
margin: 0; border: 1px solid var(--border); border-radius: var(--radius-xl);
background-color: var(--popover); color: var(--popover-foreground);
padding: 1rem; box-shadow: var(--shadow-md); min-width: 14rem;
opacity: 0; transform: translateY(-4px);
transition: opacity 150ms ease, transform 150ms ease, display 150ms allow-discrete;
/* -- Anchor positioning -- */
top: anchor(bottom); left: anchor(left); margin-top: 4px;
position-try-fallbacks: flip-block;
&:popover-open { opacity: 1; transform: translateY(0); }
}
@starting-style { .nav-menu-content:popover-open { opacity: 0; transform: translateY(-4px); } }
.nav-menu-content-link {
display: block; padding: 0.5rem 0.75rem; border-radius: var(--radius-md);
text-decoration: none; color: var(--foreground); font-size: 0.875rem;
transition: background-color 150ms ease;
&:hover { background-color: var(--accent); }
&:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
& p { margin: 0.125rem 0 0; font-size: 0.75rem; color: var(--muted-foreground); font-weight: 400; }
}
/* -- Accessibility ------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
.nav-menu-link, .nav-menu-trigger,
.nav-menu-content, .nav-menu-content-link {
transition: none;
}
.nav-menu-trigger svg { transition: none; }
}
@media (forced-colors: active) {
.nav-menu-content {
border-color: ButtonText;
}
}
}
JavaScript view file
Sets CSS anchor positioning names for trigger→content pairs. The popover API handles open/close natively.
// -- Navigation Menu -----------------------------------------
// CSS anchor positioning for dropdown navigation menus.
function init() {
document.querySelectorAll('.nav-menu:not([data-init])').forEach((nav) => {
nav.dataset.init = '';
nav.querySelectorAll('.nav-menu-trigger[popovertarget]').forEach((trigger) => {
const id = trigger.getAttribute('popovertarget');
const content = document.getElementById(id);
if (!content) return;
// CSS anchor positioning - unique name per trigger-content pair
const anchorId = `--nav-menu-${id}`;
trigger.style.anchorName = anchorId;
content.style.positionAnchor = anchorId;
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });