Mega Menu Implementation on Website
Mega menu is a navigation component that reveals a multi-column panel with grouped links, images, descriptions and additional control elements. Applied on sites with deep section structure: e-commerce, portals, corporate sites with several product lines. Standard dropdown list becomes bottleneck when there are 20+ sections and user needs context along with navigation.
When Standard Menu Stops Working
Regular dropdown list (<ul> with position: absolute) has rigid limitations:
- single column — impossible to group by categories
- no space for helper content (images, descriptions, promo blocks)
- on touch devices hover logic breaks
- keyboard management either missing or hastily implemented
Mega menu solves these problems through different reveal model and markup.
Component Architecture
Typical mega menu structure consists of three layers:
Top-level triggers — horizontal navigation bar. Each item is either a link or button revealing panel. Semantically: <button aria-expanded="false" aria-controls="menu-catalog">.
Panel — <div role="dialog"> or just <div> with aria-labelledby, absolutely or fixed positioned, spanning container full width (or viewport). Inside — CSS Grid or Flexbox with multiple columns.
Content inside panel — link groups with titles, featured blocks, images, CTA buttons. Structured through <nav> + <ul> inside named sections.
<nav aria-label="Main navigation">
<ul class="mega-nav">
<li>
<button
aria-expanded="false"
aria-controls="panel-catalog"
class="mega-nav__trigger"
>
Catalog
</button>
<div id="panel-catalog" class="mega-panel" hidden>
<div class="mega-panel__grid">
<section aria-labelledby="group-electronics">
<h3 id="group-electronics">Electronics</h3>
<ul>
<li><a href="/catalog/phones">Phones</a></li>
<li><a href="/catalog/laptops">Laptops</a></li>
</ul>
</section>
<!-- other groups -->
</div>
</div>
</li>
</ul>
</nav>
React Implementation
In React projects mega menu is usually managed through context or local state with useReducer. Open/close animations — via Framer Motion or CSS transformations.
const MegaMenu = () => {
const [activePanel, setActivePanel] = useState<string | null>(null);
const containerRef = useRef<HTMLElement>(null);
// Close on click outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setActivePanel(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Close on Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setActivePanel(null);
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<nav ref={containerRef} aria-label="Main navigation">
{navItems.map((item) => (
<MegaNavItem
key={item.id}
item={item}
isOpen={activePanel === item.id}
onToggle={() => setActivePanel(activePanel === item.id ? null : item.id)}
/>
))}
</nav>
);
};
If menu is used in Next.js with SSR, panel hidden attribute should be handled correctly on server to avoid layout shift on hydration.
Accessibility (WCAG 2.1 AA)
This is most often ignored part. Requirements are specific:
| Pattern | Implementation |
|---|---|
| Opening on Enter/Space on trigger | onKeyDown with `key === 'Enter' |
| Arrow navigation inside panel | roving tabindex or aria-activedescendant |
| Closing on Escape | global handler or via FocusTrap |
| Focus on close | return to trigger via triggerRef.current?.focus() |
| Hiding from screen reader | hidden or aria-hidden="true" on closed panel |
Library @radix-ui/react-navigation-menu implements most of these requirements out of box and is preferred base for custom mega menu in React stack. It uses NavigationMenu.Root + NavigationMenu.List + NavigationMenu.Item + NavigationMenu.Trigger + NavigationMenu.Content pattern.
Mobile Version
Mega menu on mobile devices transforms to accordion or drawer panel. This is different component, not just adapted desktop. CSS breakpoint logic:
@media (max-width: 1024px) {
.mega-panel {
position: static;
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows 0.3s ease;
}
.mega-panel[data-open="true"] {
grid-template-rows: 1fr;
}
}
Trick with grid-template-rows: 0fr → 1fr allows height animation without fixed value — established pattern for accordions without JavaScript height measurement.
Panel Positioning
Two main approaches:
Full-width — panel stretches navbar full page width, anchored to bottom of navigation bar. Used in most e-commerce sites. Implemented via position: fixed with top: <navbar-height> or via position: absolute on wrapper with overflow: visible.
Flyout — panel positioned relative to specific trigger. Fits menus with small group count. Requires position calculation via getBoundingClientRect() for correct edge behavior (flip logic, like Floating UI / Popper.js).
Performance
Mega menu panels contain many DOM nodes. If content loads from API (e.g., dynamic catalog categories), important to:
- render panels lazily — only after first open (
mountedPanels: Set<string>) - use
content-visibility: autofor hidden sections - limit number of simultaneously mounted panels
const [mounted, setMounted] = useState(false);
const handleOpen = () => {
if (!mounted) setMounted(true);
setIsOpen(true);
};
return (
<div>
<button onClick={handleOpen}>Catalog</button>
{mounted && (
<div hidden={!isOpen} className="mega-panel">
<CatalogPanelContent />
</div>
)}
</div>
);
Typical Implementation Timeline
- Static mega menu (fixed links, no API) with mobile accordion — 3–5 days
- Dynamic menu with category loading from CMS/API + full accessibility + animations — 7–10 days
- Integration into existing design system component with unit/a11y tests — add 2–3 days







