📍 🔽 ⌨️
Most headers treat dropdowns as an afterthought. A div with
hover:block, maybe a setTimeout to delay close, hope nobody hits Tab before the dropdown renders. Velocity X went the other way: megamenu that handles 10+ top-level categories, arbitrary nesting depth, full keyboard accessibility, and *stays open during Astro view transitions* because the header never unmounts.
The difference is Radix NavigationMenu. It's the only primitive that assumes you're building a *structure*, not just a trigger + popover. You get hover-to-expand, click-to-toggle, keyboard arrow nav, and focus management out of the box.
Here's the shape. Four top-level categories (Web Design, Development, Branding, Marketing) each with a 2-column mega-panel:
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger className="px-4 py-2 font-semibold">
Web Design
</NavigationMenu.Trigger>
<NavigationMenu.Content className="w-screen max-w-2xl grid grid-cols-2 gap-4 p-6">
<div>
<h3 className="font-bold mb-3">Audit & Strategy</h3>
<a href="/web-design/ux-audit/">UX Audit</a>
<a href="/web-design/messaging/">Messaging Layer</a>
</div>
<div>
<h3 className="font-bold mb-3">Design & Build</h3>
<a href="/web-design/prototyping/">Prototyping</a>
<a href="/web-design/component-system/">Component System</a>
</div>
</NavigationMenu.Content>
</NavigationMenu.Item>
{/* repeat for Development, Branding, Marketing */}
</NavigationMenu.List>
</NavigationMenu.Root>
The *gotcha*: NavigationMenu is opinionated about touch. If you're on mobile, hover doesn't exist, so Radix flips to click-based opening. That's great. The catch: you need a separate hamburger menu *and* you need to coordinate state so opening the mega doesn't also open the hamburger. Velocity X solved this with a sub-header Strip component (Shop, Search, Previous Work, About) that sits *above* the megamenu on mobile and gets its own dismiss logic. Desktop sees the megamenu. Mobile sees the hamburger + strip. The header wrapper handles the toggle.
The second gotcha is persistence. Astro's ViewTransitions are shallow DOM swaps — if your header is *inside* the transition boundary, it unmounts and remounts on every page nav. All your open-state dies. The fix is old-school: wrap your header in a *parent layout* that never remounts. The Outlet transitions, the chrome stays. This is mandatory for any app with a sticky header or persistent sidebar.
// src/layouts/Layout.astro
<HeaderApp /> {/* mounts ONCE, never remounts */}
<ViewTransitions>
<slot /> {/* Outlet for page content */}
</ViewTransitions>
Why this matters: users expect dropdowns to stay open while they're reading. They expect Escape to close. They expect arrow keys to move between menu items. NavigationMenu gives you all three. Cheaper than building it yourself, more accessible than a custom div.
The trade-off: Radix NavigationMenu is oriented toward *lists* of links. If you're trying to embed a filter widget or multiselect inside your dropdown, you're fighting the primitive. For product headers with complex interactions (cart updater, theme switcher, account menu), use DropdownMenu on a per-section basis instead. Mega = lists. Dropdowns = interactions.
The verdict: NavigationMenu is the right primitive for large, link-heavy headers. Keyboard accessibility is *required* for ecommerce and SaaS. Velocity X's choice to lock the header chrome outside the transition boundary means your nav survives page swaps. If you're shipping a header with 5+ categories, use Radix. If it's simpler (3 links + CTA), a custom div and some click handlers are fine.