Mega Menu Nav
Mega menu navbar for a luxury retail site: the panel glides between menus, with link columns, image cards and a sliding underline.
- Navbars
- Fashion & retail
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers. Drawer sub panels use the inert attribute (Chrome 102+, Safari 15.5+, Firefox 112+).
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/mega-menu-nav.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/navbars/mega-menu-nav.tsx
1/**2 * MegaMenuNav, a luxury retail header with full-width mega panels. Hover or3 * click a top item and a panel opens with columns of links plus two editorial4 * image cards. Moving between items re-measures the panel so its height5 * glides from one menu to the next while the content slides in from the side6 * you came from, and an underline slides between the top items.7 *8 * Props: brand, city, notice, menus (label, columns, cards, cardRatio),9 * utility links, bagCount.10 * CSS vars: --fd-mmn-paper, --fd-mmn-ink, --fd-mmn-accent.11 *12 * Behaviour: the header is `position: sticky` with a negative top equal to13 * the notice strip, so the strip scrolls away and the main row stays.14 * Browser support: all evergreen browsers.15 * Accessibility: top items are buttons with aria-expanded/aria-controls;16 * ArrowDown moves into the open panel, Escape closes and returns focus,17 * focus leaving the header closes it. The mobile drawer is a modal dialog18 * with a focus trap and drill-down sub panels with a Back button.19 * Reduced motion: panel height and content swap without sliding.20 */21import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";22import type { KeyboardEvent as ReactKeyboardEvent } from "react";23 24export interface MmnLink { label: string; href: string; note?: string }25export interface MmnColumn { heading: string; links: MmnLink[] }26export interface MmnCard { src: string; alt: string; kicker: string; title: string; href: string }27export interface MmnMenu { label: string; columns: MmnColumn[]; cards: [MmnCard, MmnCard]; cardRatio?: string }28export interface MegaMenuNavProps {29 brand?: string;30 city?: string;31 notice?: string;32 menus?: MmnMenu[];33 utility?: MmnLink[];34 bagCount?: number;35 className?: string;36}37 38const MENUS: MmnMenu[] = [39 {40 label: "Fragrance",41 cardRatio: "5 / 6",42 columns: [43 { heading: "Eaux de parfum", links: [44 { label: "Ambre Noir Nº 7", href: "#ambre", note: "New" },45 { label: "Thé Vert de Kyoto", href: "#the" },46 { label: "Pêche de Vigne", href: "#peche" },47 { label: "Nord", href: "#nord" },48 { label: "Crépuscule", href: "#crepuscule" },49 { label: "All fragrances", href: "#all" },50 ] },51 { heading: "By family", links: [52 { label: "Woody & amber", href: "#woody" },53 { label: "Green & tea", href: "#green" },54 { label: "Fruit & musk", href: "#fruit" },55 { label: "Cold florals", href: "#floral" },56 ] },57 { heading: "Services", links: [58 { label: "Discovery set, 8 × 2 ml", href: "#discovery" },59 { label: "Hand engraving", href: "#engrave" },60 { label: "Refill a flacon", href: "#refill" },61 ] },62 ],63 cards: [64 { src: "/img/lux/perfume-noir-800.webp", alt: "Amber glass flacon of Ambre Noir on a dark plinth", kicker: "New extrait", title: "Ambre Noir, made with labdanum from one farm in Crete", href: "#ambre" },65 { src: "/img/lux/perfume-green-800.webp", alt: "Green flacon in a pale green arch", kicker: "Bestseller", title: "Thé Vert de Kyoto, the shop's first scent since 1931", href: "#the" },66 ],67 },68 {69 label: "Home",70 cardRatio: "1 / 1",71 columns: [72 { heading: "Scent the room", links: [73 { label: "Candles, 220 g", href: "#candles" },74 { label: "Reed diffusers", href: "#reeds" },75 { label: "Room sprays", href: "#sprays" },76 ] },77 { heading: "Objects", links: [78 { label: "Stoneware vases", href: "#vases" },79 { label: "Tea service", href: "#tea" },80 { label: "Incense & holders", href: "#incense" },81 ] },82 ],83 cards: [84 { src: "/img/lux/still-vase-800.webp", alt: "White stoneware vase with an olive branch and lemons", kicker: "Objects", title: "The Ménerbes vase, thrown in Provence", href: "#vases" },85 { src: "/img/lux/still-tea-800.webp", alt: "Black teapot with two cups on a walnut shelf", kicker: "Ritual", title: "A tea service for one slow afternoon", href: "#tea" },86 ],87 },88 {89 label: "Textiles",90 cardRatio: "3 / 4",91 columns: [92 { heading: "Linen", links: [93 { label: "Throws", href: "#throws" },94 { label: "Bed linen", href: "#bed" },95 { label: "Table linen", href: "#table" },96 { label: "Curtains, made to measure", href: "#curtains" },97 ] },98 { heading: "Colours", links: [99 { label: "Oat", href: "#oat" },100 { label: "Terracotta", href: "#terra" },101 { label: "Encre", href: "#ink" },102 ] },103 { heading: "Care", links: [104 { label: "Washing linen", href: "#wash" },105 { label: "Linen water", href: "#water" },106 ] },107 ],108 cards: [109 { src: "/img/lux/drape-oat-800.webp", alt: "Folds of oat coloured linen", kicker: "Washed linen", title: "Oat, our undyed Normandy flax", href: "#oat" },110 { src: "/img/lux/drape-terra-800.webp", alt: "Folds of terracotta linen", kicker: "Garment dyed", title: "Terracotta, dyed with madder root", href: "#terra" },111 ],112 },113 {114 label: "Gifts",115 cardRatio: "3 / 2",116 columns: [117 { heading: "By price", links: [118 { label: "Under €80", href: "#u80" },119 { label: "€80 to €200", href: "#u200" },120 { label: "Over €200", href: "#o200" },121 ] },122 { heading: "Wrapped by hand", links: [123 { label: "Gift cards", href: "#cards" },124 { label: "Corporate gifting", href: "#corporate" },125 ] },126 ],127 cards: [128 { src: "/img/lux/perfume-peach-800.webp", alt: "Peach flacon in a warm arch", kicker: "Under €120", title: "Pêche de Vigne, 30 ml", href: "#peche" },129 { src: "/img/lux/still-grey-800.webp", alt: "White vase and bowls in grey light", kicker: "For the home", title: "The quiet shelf set", href: "#shelf" },130 ],131 },132 {133 label: "Maison",134 cardRatio: "16 / 10",135 columns: [136 { heading: "About", links: [137 { label: "Our atelier, rue de Saintonge", href: "#atelier" },138 { label: "The 1931 ledger", href: "#ledger" },139 { label: "Journal", href: "#journal" },140 ] },141 { heading: "Visit", links: [142 { label: "Paris, Le Marais", href: "#paris" },143 { label: "Copenhagen, Bredgade", href: "#cph" },144 { label: "Book a scent consultation", href: "#book" },145 ] },146 ],147 cards: [148 { src: "/img/lux/drape-ink-800.webp", alt: "Deep blue silk folds", kicker: "Journal", title: "Why our boxes are lined in encre silk", href: "#silk" },149 { src: "/img/lux/perfume-north-800.webp", alt: "Pale blue flacon in a white arch", kicker: "Copenhagen", title: "Nord, composed for the new store", href: "#nord" },150 ],151 },152];153 154const UTILITY: MmnLink[] = [155 { label: "Stores", href: "#stores" },156 { label: "France · EUR €", href: "#locale" },157];158 159export function MegaMenuNav({160 brand = "Vérane",161 city = "Paris",162 notice = "Complimentary hand engraving on every 100 ml flacon until 24 December",163 menus = MENUS,164 utility = UTILITY,165 bagCount = 2,166 className,167}: MegaMenuNavProps) {168 const uid = useId();169 const root = useRef<HTMLElement>(null);170 const rail = useRef<HTMLDivElement>(null);171 const bar = useRef<HTMLSpanElement>(null);172 const shell = useRef<HTMLDivElement>(null);173 const inner = useRef<HTMLDivElement>(null);174 const triggers = useRef<(HTMLButtonElement | null)[]>([]);175 const burger = useRef<HTMLButtonElement>(null);176 const drawer = useRef<HTMLDivElement>(null);177 const [active, setActive] = useState<number | null>(null);178 const [dir, setDir] = useState(0);179 const [hover, setHover] = useState<number | null>(null);180 const [mobile, setMobile] = useState(false);181 const [sub, setSub] = useState<number | null>(null);182 const timer = useRef(0);183 const prev = useRef<number | null>(null);184 185 const go = (i: number | null) => {186 window.clearTimeout(timer.current);187 if (i !== null && prev.current !== null) setDir(i > prev.current ? 1 : -1);188 else setDir(0);189 prev.current = i;190 setActive(i);191 };192 const later = (i: number | null, ms: number) => {193 window.clearTimeout(timer.current);194 timer.current = window.setTimeout(() => go(i), ms);195 };196 197 // Underline indicator follows the hovered or open item.198 const ind = hover ?? active;199 useLayoutEffect(() => {200 const b = bar.current;201 const t = ind !== null ? triggers.current[ind] : null;202 if (!b) return;203 if (!t) {204 b.style.opacity = "0";205 return;206 }207 const label = t.querySelector("span") as HTMLElement;208 b.style.opacity = "1";209 b.style.width = `${label.offsetWidth}px`;210 b.style.transform = `translateX(${t.offsetLeft + label.offsetLeft}px)`;211 }, [ind]);212 213 // Panel height follows the content of the open menu.214 useLayoutEffect(() => {215 const s = shell.current, c = inner.current;216 if (!s) return;217 if (!c || active === null) {218 s.style.height = "0px";219 return;220 }221 const set = () => (s.style.height = `${c.offsetHeight}px`);222 set();223 const ro = new ResizeObserver(set);224 ro.observe(c);225 return () => ro.disconnect();226 }, [active]);227 228 // Escape + outside click for the desktop panel.229 useEffect(() => {230 if (active === null) return;231 const onKey = (e: KeyboardEvent) => {232 if (e.key === "Escape") {233 const t = triggers.current[active];234 go(null);235 t?.focus();236 }237 };238 const onDown = (e: PointerEvent) => {239 if (root.current && !root.current.contains(e.target as Node)) go(null);240 };241 document.addEventListener("keydown", onKey);242 document.addEventListener("pointerdown", onDown);243 return () => {244 document.removeEventListener("keydown", onKey);245 document.removeEventListener("pointerdown", onDown);246 };247 }, [active]); // eslint-disable-line react-hooks/exhaustive-deps248 249 // Mobile drawer: focus in, trap, Escape.250 useEffect(() => {251 if (!mobile) return;252 const d = drawer.current;253 window.setTimeout(() => d?.querySelector<HTMLElement>("[data-first]")?.focus(), 30);254 const onKey = (e: KeyboardEvent) => {255 if (e.key === "Escape") {256 setMobile(false);257 burger.current?.focus();258 }259 if (e.key === "Tab" && d) {260 const f = Array.from(d.querySelectorAll<HTMLElement>("a,button")).filter((el) => el.offsetParent !== null && !el.closest("[inert]"));261 const i = f.indexOf(document.activeElement as HTMLElement);262 if (e.shiftKey && i <= 0) {263 e.preventDefault();264 f[f.length - 1]?.focus();265 } else if (!e.shiftKey && i === f.length - 1) {266 e.preventDefault();267 f[0]?.focus();268 }269 }270 };271 const mq = matchMedia("(min-width: 900px)");272 const onMq = () => mq.matches && setMobile(false);273 document.addEventListener("keydown", onKey);274 mq.addEventListener("change", onMq);275 return () => {276 document.removeEventListener("keydown", onKey);277 mq.removeEventListener("change", onMq);278 };279 }, [mobile]);280 281 useEffect(() => {282 if (!mobile) setSub(null);283 }, [mobile]);284 useEffect(() => {285 if (!mobile) return;286 const d = drawer.current;287 window.setTimeout(() => {288 const target = sub === null ? d?.querySelector<HTMLElement>("[data-first]") : d?.querySelector<HTMLElement>("[data-back]");289 target?.focus();290 }, 60);291 }, [sub, mobile]);292 293 const onTriggerKey = (e: ReactKeyboardEvent<HTMLButtonElement>, i: number) => {294 if (e.key === "ArrowDown") {295 e.preventDefault();296 if (active !== i) go(i);297 window.setTimeout(() => inner.current?.querySelector<HTMLElement>("a")?.focus(), 30);298 } else if (e.key === "ArrowRight" || e.key === "ArrowLeft") {299 e.preventDefault();300 const n = (i + (e.key === "ArrowRight" ? 1 : -1) + menus.length) % menus.length;301 triggers.current[n]?.focus();302 if (active !== null) go(n);303 }304 };305 306 const menu = active !== null ? menus[active] : null;307 const panelId = `${uid}-panel`;308 309 return (310 <header311 ref={root}312 className={`fd-mmn${className ? " " + className : ""}`}313 data-open={active !== null ? "" : undefined}314 onPointerLeave={(e) => e.pointerType === "mouse" && later(null, 180)}315 onPointerEnter={() => window.clearTimeout(timer.current)}316 onBlur={(e) => {317 if (!root.current?.contains(e.relatedTarget as Node) && !mobile) go(null);318 }}319 >320 <style>{css}</style>321 <div className="fd-mmn-notice">322 <p>{notice}</p>323 <div className="fd-mmn-util">324 {utility.map((u) => <a key={u.href} href={u.href}>{u.label}</a>)}325 </div>326 </div>327 328 <div className="fd-mmn-main">329 <button ref={burger} type="button" className="fd-mmn-burger" aria-label="Open menu" aria-expanded={mobile} aria-controls={`${uid}-drawer`} onClick={() => setMobile(true)}>330 <svg width="22" height="22" viewBox="0 0 22 22" aria-hidden="true"><path d="M3 7h16M3 15h10" stroke="currentColor" strokeWidth="1.3" fill="none" /></svg>331 </button>332 <nav className="fd-mmn-nav" aria-label="Shop">333 <div ref={rail} className="fd-mmn-rail" onPointerLeave={() => setHover(null)}>334 {menus.map((m, i) => (335 <button336 key={m.label}337 ref={(el) => { triggers.current[i] = el; }}338 type="button"339 className="fd-mmn-top"340 aria-expanded={active === i}341 aria-controls={panelId}342 data-on={active === i ? "" : undefined}343 onPointerEnter={(e) => {344 if (e.pointerType !== "mouse") return;345 setHover(i);346 if (active === null) later(i, 90);347 else go(i);348 }}349 onClick={() => (active === i ? go(null) : go(i))}350 onKeyDown={(e) => onTriggerKey(e, i)}351 >352 <span>{m.label}</span>353 </button>354 ))}355 <span ref={bar} className="fd-mmn-bar" aria-hidden="true" />356 </div>357 </nav>358 359 <a className="fd-mmn-brand" href="#top" aria-label={`${brand} ${city}, home`}>360 <span className="fd-mmn-word">{brand}</span>361 <span className="fd-mmn-city">{city}</span>362 </a>363 364 <div className="fd-mmn-tools">365 <a href="#search" className="fd-mmn-tool fd-mmn-hide-sm">366 <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><circle cx="8" cy="8" r="5.5" fill="none" stroke="currentColor" strokeWidth="1.2" /><path d="M12 12l4 4" stroke="currentColor" strokeWidth="1.2" /></svg>367 <span>Search</span>368 </a>369 <a href="#account" className="fd-mmn-tool fd-mmn-hide-sm">Account</a>370 <a href="#bag" className="fd-mmn-tool fd-mmn-bag" aria-label={`Bag, ${bagCount} items`}>371 <span className="fd-mmn-bagtxt">Bag</span>372 <span className="fd-mmn-count">{bagCount}</span>373 </a>374 </div>375 </div>376 377 <div ref={shell} id={panelId} className="fd-mmn-shell" role="region" aria-label={menu ? `${menu.label} menu` : undefined}>378 {menu && (379 <div ref={inner} key={active} className="fd-mmn-panel" data-dir={dir} onKeyDown={(e) => {380 if (e.key === "ArrowUp" && active !== null && (e.target as HTMLElement) === inner.current?.querySelector("a")) {381 e.preventDefault();382 triggers.current[active]?.focus();383 }384 }}>385 <div className="fd-mmn-cols">386 {menu.columns.map((c) => (387 <div key={c.heading} className="fd-mmn-col">388 <p className="fd-mmn-h">{c.heading}</p>389 <ul>390 {c.links.map((l) => (391 <li key={l.href}>392 <a href={l.href} className="fd-mmn-link">{l.label}{l.note && <em>{l.note}</em>}</a>393 </li>394 ))}395 </ul>396 </div>397 ))}398 </div>399 <div className="fd-mmn-cards">400 {menu.cards.map((c) => (401 <a key={c.href + c.src} href={c.href} className="fd-mmn-card">402 <span className="fd-mmn-ph" style={{ aspectRatio: menu.cardRatio ?? "4 / 5" }}>403 <img src={c.src} alt={c.alt} width={800} height={1000} loading="lazy" />404 </span>405 <span className="fd-mmn-kick">{c.kicker}</span>406 <span className="fd-mmn-ct">{c.title}</span>407 </a>408 ))}409 </div>410 </div>411 )}412 </div>413 <div className="fd-mmn-scrim" aria-hidden="true" onPointerEnter={() => later(null, 60)} onClick={() => go(null)} />414 415 {/* Mobile drawer */}416 <div className="fd-mmn-dwrap" data-show={mobile ? "" : undefined} hidden={!mobile}>417 <div className="fd-mmn-dscrim" onClick={() => setMobile(false)} aria-hidden="true" />418 <div ref={drawer} id={`${uid}-drawer`} className="fd-mmn-drawer" role="dialog" aria-modal="true" aria-label="Menu" data-sub={sub !== null ? "" : undefined}>419 <div className="fd-mmn-dhead">420 <span className="fd-mmn-word fd-mmn-dword">{brand}</span>421 <button type="button" className="fd-mmn-x" aria-label="Close menu" onClick={() => { setMobile(false); burger.current?.focus(); }}>422 <svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M4 4l12 12M16 4L4 16" stroke="currentColor" strokeWidth="1.3" /></svg>423 </button>424 </div>425 <div className="fd-mmn-track">426 <div className="fd-mmn-pane" inert={sub !== null ? true : undefined}>427 <ul className="fd-mmn-dlist">428 {menus.map((m, i) => (429 <li key={m.label}>430 <button type="button" className="fd-mmn-dtop" data-first={i === 0 ? "" : undefined} aria-expanded={sub === i} onClick={() => setSub(i)}>431 <span>{m.label}</span>432 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.2" /></svg>433 </button>434 </li>435 ))}436 </ul>437 <div className="fd-mmn-dutil">438 <a href="#account">Account</a>439 <a href="#search">Search</a>440 {utility.map((u) => <a key={u.href} href={u.href}>{u.label}</a>)}441 </div>442 <p className="fd-mmn-dnote">{notice}</p>443 </div>444 <div className="fd-mmn-pane" inert={sub === null ? true : undefined}>445 {sub !== null && (446 <>447 <button type="button" className="fd-mmn-back" data-back="" onClick={() => setSub(null)}>448 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M10 3L5 8l5 5" fill="none" stroke="currentColor" strokeWidth="1.2" /></svg>449 All450 </button>451 <p className="fd-mmn-dsubt">{menus[sub].label}</p>452 <a href={menus[sub].cards[0].href} className="fd-mmn-dcard">453 <img src={menus[sub].cards[0].src} alt={menus[sub].cards[0].alt} width={800} height={1000} loading="lazy" />454 <span>{menus[sub].cards[0].kicker}</span>455 </a>456 {menus[sub].columns.map((c) => (457 <div key={c.heading} className="fd-mmn-dgroup">458 <p className="fd-mmn-h">{c.heading}</p>459 <ul>460 {c.links.map((l) => <li key={l.href}><a href={l.href} onClick={() => setMobile(false)}>{l.label}{l.note && <em>{l.note}</em>}</a></li>)}461 </ul>462 </div>463 ))}464 </>465 )}466 </div>467 </div>468 </div>469 </div>470 </header>471 );472}473 474const serif = `"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif`;475const sans = `ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif`;476 477const css = `478.fd-mmn {479 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);480 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);481 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);482 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;483 --fd-mmn-paper: #f4f0e8;484 --fd-mmn-ink: #1b1815;485 --fd-mmn-soft: #6f675d;486 --fd-mmn-line: #ddd5c8;487 --fd-mmn-accent: #6e1f1c;488 position: sticky; top: -34px; z-index: 50; display: block; margin: 0; padding: 0;489 font: 400 15px/1.4 ${sans};490 color: var(--fd-mmn-ink); background: var(--fd-mmn-paper);491}492.fd-mmn, .fd-mmn *, .fd-mmn *::before, .fd-mmn *::after { box-sizing: border-box; }493.fd-mmn a:focus-visible, .fd-mmn button:focus-visible { outline: 2px solid color-mix(in srgb, var(--fd-mmn-accent) 70%, transparent); outline-offset: 2px; }494 495.fd-mmn-notice { display: flex; align-items: center; justify-content: center; height: 34px; margin: 0; padding: 0 32px; background: var(--fd-mmn-ink); color: #efe8db; position: relative; }496.fd-mmn-notice p { margin: 0; padding: 0; font: 400 0.75rem/1.2 ${sans}; letter-spacing: 0.02em; color: #efe8db; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }497.fd-mmn-util { position: absolute; right: 32px; top: 0; height: 100%; display: flex; align-items: center; gap: 20px; }498.fd-mmn-util a { font: 400 0.75rem/1.2 ${sans}; color: #cfc6b7; text-decoration: none; }499.fd-mmn-util a:hover { color: #fff; }500 501.fd-mmn-main { position: relative; z-index: 3; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 76px; margin: 0; padding: 0 32px; background: var(--fd-mmn-paper); border-bottom: 1px solid var(--fd-mmn-line); }502.fd-mmn-nav { margin: 0; padding: 0; }503.fd-mmn-rail { position: relative; display: flex; align-items: center; gap: 2px; margin: 0 0 0 -12px; padding: 0; }504.fd-mmn-top { position: relative; display: block; margin: 0; padding: 10px 12px; border: 0; background: none; font: 400 1rem/1.2 ${serif}; letter-spacing: 0; color: var(--fd-mmn-ink); cursor: pointer; appearance: none; -webkit-appearance: none; border-radius: 2px; }505.fd-mmn-top span { display: inline-block; }506.fd-mmn-bar { position: absolute; left: 0; bottom: 2px; height: 1px; width: 0; background: var(--fd-mmn-ink); opacity: 0; pointer-events: none; transition: transform var(--fd-dur-3) var(--fd-ease-out), width var(--fd-dur-3) var(--fd-ease-out), opacity var(--fd-dur-2) linear; }507.fd-mmn-top[data-on] { color: var(--fd-mmn-accent); }508 509.fd-mmn-brand { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 0; padding: 4px 8px; text-decoration: none; color: var(--fd-mmn-ink); }510.fd-mmn-word { display: block; font: 400 1.875rem/1 ${serif}; letter-spacing: 0.28em; text-transform: uppercase; color: var(--fd-mmn-ink); padding-left: 0.28em; }511.fd-mmn-city { display: block; font: 500 0.625rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.32em; text-transform: uppercase; color: var(--fd-mmn-soft); padding-left: 0.32em; }512 513.fd-mmn-tools { display: flex; align-items: center; justify-content: flex-end; gap: 22px; margin: 0; padding: 0; }514.fd-mmn-tool { display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 6px 0; font: 400 0.875rem/1.2 ${sans}; color: var(--fd-mmn-ink); text-decoration: none; background-image: linear-gradient(currentColor, currentColor); background-size: 0% 1px; background-repeat: no-repeat; background-position: 0 100%; transition: background-size var(--fd-dur-2) var(--fd-ease-out); }515.fd-mmn-tool svg { display: block; }516.fd-mmn-tool:hover { background-size: 100% 1px; }517.fd-mmn-count { display: inline-grid; place-items: center; min-width: 20px; height: 20px; margin: 0; padding: 0 5px; border-radius: 9999px; font: 500 0.6875rem/1 ${sans}; color: #f4f0e8; background: var(--fd-mmn-accent); }518 519.fd-mmn-shell { position: absolute; left: 0; right: 0; top: 100%; z-index: 2; height: 0; overflow: hidden; margin: 0; padding: 0; background: var(--fd-mmn-paper); transition: height var(--fd-dur-3) var(--fd-ease-in-out); box-shadow: 0 30px 40px -30px rgba(27,24,21,0.18); }520.fd-mmn-panel { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: 56px; margin: 0; padding: 36px 32px 44px; animation: fd-mmn-in var(--fd-dur-3) var(--fd-ease-out); }521.fd-mmn-panel[data-dir="1"] { animation-name: fd-mmn-inr; }522.fd-mmn-panel[data-dir="-1"] { animation-name: fd-mmn-inl; }523@keyframes fd-mmn-in { from { opacity: 0; transform: translateY(-6px); } }524@keyframes fd-mmn-inr { from { opacity: 0; transform: translateX(24px); } }525@keyframes fd-mmn-inl { from { opacity: 0; transform: translateX(-24px); } }526.fd-mmn-cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; margin: 0; padding: 0; }527.fd-mmn-h { margin: 0 0 14px; padding: 0 0 10px; border-bottom: 1px solid var(--fd-mmn-line); font: 500 0.6875rem/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fd-mmn-soft); }528.fd-mmn-col ul, .fd-mmn-dgroup ul { margin: 0; padding: 0; list-style: none; }529.fd-mmn-col li { margin: 0; padding: 0; }530.fd-mmn-link { display: inline-flex; align-items: baseline; gap: 8px; margin: 0; padding: 5px 0; font: 400 1.0625rem/1.35 ${serif}; color: var(--fd-mmn-ink); text-decoration: none; transition: transform var(--fd-dur-2) var(--fd-ease-out), color var(--fd-dur-1) linear; }531.fd-mmn-link:hover, .fd-mmn-link:focus-visible { transform: translateX(4px); color: var(--fd-mmn-accent); }532.fd-mmn-link em, .fd-mmn-dgroup em { font: italic 400 0.8125rem/1 ${serif}; color: var(--fd-mmn-accent); }533.fd-mmn-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 0; padding: 0; align-items: start; }534.fd-mmn-card { display: block; margin: 0; padding: 0; color: var(--fd-mmn-ink); text-decoration: none; }535.fd-mmn-ph { display: block; overflow: hidden; margin: 0 0 12px; background: #e6dfd2; }536.fd-mmn-ph img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform var(--fd-dur-4) var(--fd-ease-out); }537.fd-mmn-card:hover .fd-mmn-ph img { transform: scale(1.04); }538.fd-mmn-kick { display: block; margin: 0 0 4px; font: 500 0.6875rem/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fd-mmn-accent); }539.fd-mmn-ct { display: block; font: italic 400 1rem/1.35 ${serif}; color: var(--fd-mmn-ink); max-width: 30ch; }540 541.fd-mmn-scrim { position: fixed; inset: 0; z-index: 1; background: rgba(27,24,21,0.28); opacity: 0; pointer-events: none; transition: opacity var(--fd-dur-3) var(--fd-ease-out); }542.fd-mmn[data-open] .fd-mmn-scrim { opacity: 1; pointer-events: auto; }543 544.fd-mmn-burger { display: none; width: 40px; height: 40px; margin: 0 0 0 -10px; padding: 9px; border: 0; background: none; color: var(--fd-mmn-ink); cursor: pointer; appearance: none; -webkit-appearance: none; }545.fd-mmn-burger svg { display: block; }546 547.fd-mmn-dwrap { position: fixed; inset: 0; z-index: 60; }548.fd-mmn-dwrap[hidden] { display: none; }549.fd-mmn-dscrim { position: absolute; inset: 0; background: rgba(27,24,21,0.4); animation: fd-mmn-fade var(--fd-dur-3) var(--fd-ease-out); }550@keyframes fd-mmn-fade { from { opacity: 0; } }551.fd-mmn-drawer { position: absolute; left: 0; top: 0; bottom: 0; width: min(420px, 100%); display: flex; flex-direction: column; margin: 0; padding: 0; background: var(--fd-mmn-paper); overflow: hidden; animation: fd-mmn-slide var(--fd-dur-3) var(--fd-ease-out); }552@keyframes fd-mmn-slide { from { transform: translateX(-100%); } }553.fd-mmn-dhead { flex: none; display: flex; align-items: center; justify-content: space-between; height: 64px; margin: 0; padding: 0 12px 0 20px; border-bottom: 1px solid var(--fd-mmn-line); }554.fd-mmn-dword { font-size: 1.25rem; }555.fd-mmn-x { display: grid; place-items: center; width: 44px; height: 44px; margin: 0; padding: 0; border: 0; background: none; color: var(--fd-mmn-ink); cursor: pointer; appearance: none; -webkit-appearance: none; }556.fd-mmn-x svg { display: block; }557.fd-mmn-track { position: relative; flex: 1; min-height: 0; display: grid; grid-template-columns: 100% 100%; margin: 0; padding: 0; transition: transform var(--fd-dur-3) var(--fd-ease-in-out); }558.fd-mmn-drawer[data-sub] .fd-mmn-track { transform: translateX(-100%); }559.fd-mmn-pane { min-height: 0; overflow-y: auto; margin: 0; padding: 12px 20px 32px; }560.fd-mmn-dlist { margin: 0; padding: 0; list-style: none; }561.fd-mmn-dlist li { margin: 0; padding: 0; border-bottom: 1px solid var(--fd-mmn-line); }562.fd-mmn-dtop { display: flex; width: 100%; align-items: center; justify-content: space-between; margin: 0; padding: 18px 0; border: 0; background: none; font: 400 1.75rem/1.1 ${serif}; letter-spacing: -0.01em; color: var(--fd-mmn-ink); text-align: left; cursor: pointer; appearance: none; -webkit-appearance: none; }563.fd-mmn-dtop svg { display: block; color: var(--fd-mmn-soft); }564.fd-mmn-dutil { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; margin: 28px 0 0; padding: 0; }565.fd-mmn-dutil a { font: 400 0.9375rem/1.3 ${sans}; color: var(--fd-mmn-ink); text-decoration: none; }566.fd-mmn-dnote { margin: 28px 0 0; padding: 14px 0 0; border-top: 1px solid var(--fd-mmn-line); font: italic 400 0.9375rem/1.4 ${serif}; color: var(--fd-mmn-soft); }567.fd-mmn-back { display: inline-flex; align-items: center; gap: 6px; margin: 4px 0 14px -4px; padding: 6px 4px; border: 0; background: none; font: 500 0.75rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fd-mmn-soft); cursor: pointer; appearance: none; -webkit-appearance: none; }568.fd-mmn-back svg { display: block; }569.fd-mmn-dsubt { margin: 0 0 16px; font: italic 400 2.25rem/1 ${serif}; letter-spacing: -0.02em; color: var(--fd-mmn-ink); }570.fd-mmn-dcard { position: relative; display: block; margin: 0 0 24px; height: 180px; overflow: hidden; text-decoration: none; }571.fd-mmn-dcard img { display: block; width: 100%; height: 100%; object-fit: cover; }572.fd-mmn-dcard span { position: absolute; left: 12px; bottom: 12px; padding: 5px 8px; font: 500 0.6875rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fd-mmn-ink); background: var(--fd-mmn-paper); }573.fd-mmn-dgroup { margin: 0 0 24px; padding: 0; }574.fd-mmn-dgroup li { margin: 0; padding: 0; }575.fd-mmn-dgroup a { display: flex; align-items: baseline; gap: 8px; margin: 0; padding: 9px 0; font: 400 1.125rem/1.3 ${serif}; color: var(--fd-mmn-ink); text-decoration: none; }576 577@media (max-width: 1180px) { .fd-mmn-util { display: none; } .fd-mmn-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }578@media (max-width: 899px) {579 .fd-mmn { top: -30px; }580 .fd-mmn-notice { height: 30px; padding: 0 16px; }581 .fd-mmn-notice p { font-size: 0.6875rem; }582 .fd-mmn-main { height: 64px; padding: 0 16px; grid-template-columns: 44px 1fr 44px; }583 .fd-mmn-nav, .fd-mmn-shell, .fd-mmn-scrim, .fd-mmn-hide-sm, .fd-mmn-bagtxt { display: none; }584 .fd-mmn-burger { display: block; }585 .fd-mmn-word { font-size: 1.375rem; }586 .fd-mmn-city { display: none; }587 .fd-mmn-tools { gap: 0; }588}589@media (prefers-reduced-motion: reduce) {590 .fd-mmn-shell, .fd-mmn-bar, .fd-mmn-track, .fd-mmn-link, .fd-mmn-ph img { transition: none; }591 .fd-mmn-panel, .fd-mmn-drawer, .fd-mmn-dscrim { animation: none; }592}593`;594 595/* ── Demo ─────────────────────────────────────────────────────────────── */596 597const PRODUCTS = [598 { src: "/img/lux/perfume-green-800.webp", name: "Thé Vert de Kyoto", meta: "Eau de parfum · 50 ml", price: "€185" },599 { src: "/img/lux/perfume-peach-800.webp", name: "Pêche de Vigne", meta: "Eau de parfum · 50 ml", price: "€185" },600 { src: "/img/lux/perfume-north-800.webp", name: "Nord", meta: "Eau de parfum · 50 ml", price: "€195" },601 { src: "/img/lux/perfume-dusk-800.webp", name: "Crépuscule", meta: "Eau de parfum · 50 ml", price: "€195" },602];603 604export function Demo() {605 return (606 <div className="fd-mmn-demo">607 <style>{demoCss}</style>608 <MegaMenuNav />609 <main>610 <section className="fd-mmn-demo-hero">611 <div className="fd-mmn-demo-copy">612 <p className="fd-mmn-demo-k">Nº 7 · Extrait de parfum</p>613 <h1>Ambre <i>Noir</i></h1>614 <p className="fd-mmn-demo-lede">Labdanum, smoked black tea and a little cracked pepper. Macerated for nine weeks in our cellar on rue de Saintonge before it is bottled by hand.</p>615 <dl>616 <div><dt>Size</dt><dd>50 ml</dd></div>617 <div><dt>Price</dt><dd>€265</dd></div>618 <div><dt>Batch</dt><dd>0412 of 0600</dd></div>619 </dl>620 <a href="#add" className="fd-mmn-demo-btn">Add to bag</a>621 </div>622 <img className="fd-mmn-demo-img" src="/img/lux/perfume-noir.webp" alt="Ambre Noir flacon on a dark plinth" width={1200} height={1500} />623 </section>624 <section className="fd-mmn-demo-row" aria-label="More eaux de parfum">625 {PRODUCTS.map((p) => (626 <a key={p.name} href="#p" className="fd-mmn-demo-p">627 <img src={p.src} alt={`${p.name} flacon`} width={800} height={1000} loading="lazy" />628 <b>{p.name}</b>629 <span>{p.meta}</span>630 <span>{p.price}</span>631 </a>632 ))}633 </section>634 </main>635 </div>636 );637}638 639const demoCss = `640.fd-mmn-demo { margin: 0; padding: 0; min-height: 100vh; background: #f4f0e8; color: #1b1815; font: 400 15px/1.5 ${sans}; }641.fd-mmn-demo *, .fd-mmn-demo *::before, .fd-mmn-demo *::after { box-sizing: border-box; }642.fd-mmn-demo main { display: block; margin: 0; padding: 0 32px 120px; }643.fd-mmn-demo-hero { display: grid; grid-template-columns: 1fr 1.1fr; gap: 48px; align-items: end; margin: 0; padding: 48px 0 64px; border-bottom: 1px solid #ddd5c8; }644.fd-mmn-demo-k { margin: 0 0 18px; font: 500 0.75rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: #6e1f1c; }645.fd-mmn-demo h1 { margin: 0; font: 400 clamp(4rem, 11vw, 9.5rem)/0.86 ${serif}; letter-spacing: -0.045em; color: #1b1815; }646.fd-mmn-demo h1 i { font-style: italic; }647.fd-mmn-demo-lede { max-width: 42ch; margin: 28px 0 28px; font: 400 1.0625rem/1.55 ${serif}; color: #4a443d; }648.fd-mmn-demo dl { display: flex; gap: 36px; margin: 0 0 32px; padding: 16px 0; border-top: 1px solid #ddd5c8; border-bottom: 1px solid #ddd5c8; }649.fd-mmn-demo dt { margin: 0 0 4px; font: 500 0.6875rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; color: #6f675d; }650.fd-mmn-demo dd { margin: 0; font: 400 1.0625rem/1.2 ${serif}; }651.fd-mmn-demo-btn { display: inline-block; padding: 16px 36px; font: 500 0.8125rem/1 ${sans}; letter-spacing: 0.12em; text-transform: uppercase; color: #f4f0e8; background: #1b1815; text-decoration: none; }652.fd-mmn-demo-img { display: block; width: 100%; height: 640px; object-fit: cover; }653.fd-mmn-demo-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 0; padding: 56px 0 0; }654.fd-mmn-demo-p { display: flex; flex-direction: column; gap: 2px; color: #1b1815; text-decoration: none; }655.fd-mmn-demo-p img { display: block; width: 100%; aspect-ratio: 4 / 5; height: auto; object-fit: cover; margin: 0 0 12px; }656.fd-mmn-demo-p b { font: 400 1.125rem/1.3 ${serif}; }657.fd-mmn-demo-p span { font: 400 0.8125rem/1.4 ${sans}; color: #6f675d; }658@media (max-width: 899px) {659 .fd-mmn-demo main { padding: 0 16px 80px; }660 .fd-mmn-demo-hero { grid-template-columns: 1fr; gap: 28px; padding: 28px 0 40px; }661 .fd-mmn-demo-img { height: 420px; order: -1; }662 .fd-mmn-demo-row { grid-template-columns: 1fr 1fr; gap: 14px; }663 .fd-mmn-demo dl { gap: 22px; }664}665`;More Navbars. Same file contract.
Questions about Mega Menu Nav
Run npx shadcn@latest add https://freedesignmd.com/r/mega-menu-nav.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.
Mega menu navbar for a luxury retail site: the panel glides between menus, with link columns, image cards and a sliding underline. It is a free, MIT-licensed React navbars component from the freedesignmd library.
Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.
All evergreen browsers. Drawer sub panels use the inert attribute (Chrome 102+, Safari 15.5+, Firefox 112+).
Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.


