Mega menu
A site header whose top-level items open a wide panel.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialGive each panel a width in its own class, and a margin-top for the gap under the bar: the engine only decides where a panel goes. Keep the entries of the row unpositioned, with no transform, give the header side paddings, which are the margins a panel never crosses, an opaque background and a z-index above the page. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Tune it
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the section on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-mega-menu Goes on the header and takes no value: leave the value field empty. It marks the block the engine mounts on, which carries the settings, the mode and the state, and against which every panel and the surface are placed. Without it on any element of the page, the embed marks the header around the first item itself. data-mega-menu-item Marks one entry of the row, the block holding a trigger and, when it has one, its panel. An entry without a panel is a plain link of the row. Without it on any block the menu is not mounted, and the console says so. [ Required ] data-mega-menu-trigger Marks what opens the panel of its entry, a link to the section's own page. With the script it becomes a button, with aria-expanded and aria-controls. Without it the first link or button of the entry is taken. [ Optional ] data-mega-menu-panel Marks the panel of an entry, placed right after its trigger. Its width is its own class. An entry without it is a plain link; a menu where no entry has one is not mounted. [ Required ] data-mega-menu-content Marks the one block inside a panel that holds everything it shows: it is what crosses sideways when the panel changes item, and what folds on a phone. Without it the first block of the panel is taken. [ Optional ] data-mega-menu-link Marks what arrives one after the other inside a panel, the links as a rule. It also draws the accent line under the pointer. Without it on anything in a panel, every link of that panel is taken. [ Optional ] data-mega-menu-feature Marks the frame of a featured picture, a box holding an image: the frame is uncovered from the side the panel arrives from while the picture settles to its size. Without it nothing is uncovered. [ Optional ] data-mega-menu-list Marks the row of entries, which becomes the sheet on a phone. Without it the parent of the first entry is taken. [ Optional ] data-mega-menu-toggle Marks the button that opens and closes the sheet on a phone, hidden on a wide window. Without it nothing opens the phone menu but JavaScript. [ Optional ] data-mega-menu-surface Marks the one box behind every panel, the one that morphs: its colour, radius and shadow are its class. It sits in the header, after the row. Without it the engine builds one, cream with a 14px radius. [ Optional ] data-mega-menu-mode, data-mega-menu-state Written by the engine on the header: the mode, desktop or phone, and the state, closed, opening, open or closing. The stylesheet lays the menu out from them. Never written by hand. [ Optional ] data-mega-menu-active, data-mega-menu-leaving Written by the engine on an entry: active on the one whose panel shows, or whose accordion is open, leaving on the one it replaces while its content goes. Never written by hand. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same section. An attribute always wins over the defaults written in the code.Attribute Default What it is data-mega-menu-accent #ffab1a Colour of the line drawn under a link of a panel while the pointer or the keyboard is on it, written on the menu as --mega-menu-accent. Options: any CSS colour; set it empty and a --mega-menu-accent of the stylesheet takes over. data-mega-menu-open 0.6 Time the panel takes to unfold from the bar when nothing was open, in seconds; the links and the picture are timed from it. data-mega-menu-close 0.35 Time the panel takes to fold back up, in seconds. data-mega-menu-morph 0.55 Time the surface takes to travel from one item's panel to another's, in seconds; the cross of the content, the links and the picture are timed from it, and so are the accordions of the phone menu. data-mega-menu-open-ease power4.out Shape of the unfolding, a GSAP ease name. Options: any GSAP ease; an out ease answers at once and settles, inOut starts late under a pointer. data-mega-menu-morph-ease power3.inOut Shape of the morph, a GSAP ease name; the phone sheet drops on the same one. Options: any GSAP ease; inOut reads as one box that travels, an out ease as a box that snaps then drifts. data-mega-menu-slide clamp(20px, 2.6vw, 44px) How far the content travels sideways when the panel changes item, any CSS length: the old content leaves that far towards the side the pointer came from, the new one comes in from the other. data-mega-menu-rise clamp(8px, 1.3vh, 14px) Height a link comes up from as it arrives, any CSS length; the rows of the phone sheet rise from twice that. data-mega-menu-stagger 0.035 Delay between two links arriving, in seconds. data-mega-menu-hover true Whether a pointer resting on an item opens its panel. Touch never does: a tap is a click. Options: true; false, and the panels open on a click or from the keyboard only. data-mega-menu-hover-delay 0.09 How long the pointer has to rest on an item before its panel opens, in seconds. An item crossed on the way down to the open panel waits longer on its own. data-mega-menu-close-delay 0.22 How long the pointer may stay off the menu before it closes, in seconds; it is what carries the pointer across the gap between the bar and the panel. data-mega-menu-breakpoint 767 Widest window, in px, that gets the phone menu: the button, the sheet and the accordions. The look of the preview changes at the same width, 767, in the stylesheet. data-mega-menu-escape true Whether Escape closes the menu and hands the focus back to the item. Options: true; false. data-mega-menu-close-on-outside true Whether a click outside the menu closes it. Options: true; false. data-mega-menu-close-on-link true Whether a click on a link of a panel closes the menu, for a link that stays on the page. Options: true; false. data-mega-menu-label-open Menu Text of the phone button while the menu is closed. Not written when the button holds an element, an icon for one. Options: any text. data-mega-menu-label-close Close Text of the phone button while the menu is open. Options: any text.
Two files and their pictures (4), no build step. It also needs GSAP 3 on the page for the motion, loaded with the tag in step 1. Without it the menu still opens, morphs and closes, at once and with no motion.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
MegaMenu.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-mega-menu-accent #ffab1a Colour of the line drawn under a link of a panel while the pointer or the keyboard is on it, written on the menu as --mega-menu-accent. Options: any CSS colour; set it empty and a --mega-menu-accent of the stylesheet takes over. data-mega-menu-open 0.6 Time the panel takes to unfold from the bar when nothing was open, in seconds; the links and the picture are timed from it. Under 0.35 the panel is simply there and the links arrive all at once Past 0.9 the menu feels slow to answer a pointer data-mega-menu-close 0.35 Time the panel takes to fold back up, in seconds. Under 0.2 it vanishes with no fold Past 0.6 the panel lingers over the page after the pointer has left data-mega-menu-morph 0.55 Time the surface takes to travel from one item's panel to another's, in seconds; the cross of the content, the links and the picture are timed from it, and so are the accordions of the phone menu. Under 0.35 the surface jumps and the cross is not read Past 0.8 sweeping the pointer along the row leaves the panel trailing behind data-mega-menu-open-ease power4.out Shape of the unfolding, a GSAP ease name. Options: any GSAP ease; an out ease answers at once and settles, inOut starts late under a pointer. data-mega-menu-morph-ease power3.inOut Shape of the morph, a GSAP ease name; the phone sheet drops on the same one. Options: any GSAP ease; inOut reads as one box that travels, an out ease as a box that snaps then drifts. data-mega-menu-slide clamp(20px, 2.6vw, 44px) How far the content travels sideways when the panel changes item, any CSS length: the old content leaves that far towards the side the pointer came from, the new one comes in from the other. At 0 the two contents fade through each other in place Past 80px the content visibly flies across a narrow panel data-mega-menu-rise clamp(8px, 1.3vh, 14px) Height a link comes up from as it arrives, any CSS length; the rows of the phone sheet rise from twice that. At 0 the links only fade in Past 24px the links jump rather than settle data-mega-menu-stagger 0.035 Delay between two links arriving, in seconds. At 0 every link arrives together Past 0.07 the last link of a long panel is still arriving after the surface has landed data-mega-menu-hover true Whether a pointer resting on an item opens its panel. Touch never does: a tap is a click. Options: true; false, and the panels open on a click or from the keyboard only. data-mega-menu-hover-delay 0.09 How long the pointer has to rest on an item before its panel opens, in seconds. An item crossed on the way down to the open panel waits longer on its own. At 0 a pointer that only crosses the bar flashes a panel Past 0.25 the menu feels deaf data-mega-menu-close-delay 0.22 How long the pointer may stay off the menu before it closes, in seconds; it is what carries the pointer across the gap between the bar and the panel. Under 0.1 the panel closes while the pointer crosses the gap Past 0.5 the panel stays over the page long after the pointer has left data-mega-menu-breakpoint 767 Widest window, in px, that gets the phone menu: the button, the sheet and the accordions. The look of the preview changes at the same width, 767, in the stylesheet. Under 600 a tablet in portrait keeps the row and its panels, which have to fit Past 767 change the @media of the look to match, or the sheet opens with the wide look data-mega-menu-escape true Whether Escape closes the menu and hands the focus back to the item. Options: true; false. data-mega-menu-close-on-outside true Whether a click outside the menu closes it. Options: true; false. data-mega-menu-close-on-link true Whether a click on a link of a panel closes the menu, for a link that stays on the page. Options: true; false. data-mega-menu-label-open Menu Text of the phone button while the menu is closed. Not written when the button holds an element, an icon for one. Options: any text. data-mega-menu-label-close Close Text of the phone button while the menu is open. Options: any text. -
Drive it from JavaScript
When something other than the header has to open a panel, a search key or a call to action for one, or when the page reacts to the panel that shows.const element = document.querySelector('[data-mega-menu]'); element.addEventListener('megamenu:change', (e) => { console.log(e.detail); }); const menu = MegaMenu.mount(element); // the header auto() mounted, or a new one menu.open(1); // the panel of the second entry; menu.close(), menu.toggle(1) menu.index(); // the entry whose panel shows, -1 when none element.addEventListener('megamenu:change', (e) => { console.log(e.detail.index, e.detail.from); }); // When the header leaves the page: menu.destroy(); // closes at once and gives the markup back as it was
-
Fit it to your page
- Structure: the header carries
data-mega-menu. Each entry of its row carriesdata-mega-menu-itemand holds its trigger,data-mega-menu-trigger, then its panel,data-mega-menu-panel, with one block inside,data-mega-menu-content, around everything the panel shows. The panel follows its trigger in the markup, which is what gives the keyboard its order. An entry without a panel is a plain link of the row. - Inside a panel,
data-mega-menu-linkmarks what arrives one after the other, the links as a rule, anddata-mega-menu-featurethe frame of a picture, a box holding animg. Both are optional: without the first every link of the panel is taken, without the second nothing is uncovered. - The surface,
data-mega-menu-surface, is the one box behind every panel: its colour, its radius and its shadow are a class of yours. It sits in the header, after the row. Without it the engine builds one, in cream with a 14px radius. - The size of a panel is its own. Give each panel a width in its class, and a
margin-topfor the gap under the bar. The engine reads the size at each opening and only decides the place: centred under its trigger, and kept inside the side paddings of the header. - The trigger is a real link. Give it the
hrefof the section's own page. With the script it becomes a button,role="button",aria-expandedandaria-controls, and a click opens the panel instead of following the link. Without the script the link works, and the panel shows under the pointer or the keyboard through the stylesheet alone. - On a phone, at or under
breakpoint, the row,data-mega-menu-list, becomes a sheet under the bar that fills the rest of the window, anddata-mega-menu-togglemarks the button that opens it. The panels fold under their triggers, one open at a time. The page behind is held while the sheet is open. The button is hidden on a wide window. - Pointer: resting on an item opens its panel after
hoverDelay, moving to another morphs the panel there, leaving the menu closes it aftercloseDelay. A click on the item under the pointer pins the panel, which then stays until a second click, Escape, a link or a click outside. Touch opens on a tap and closes on a second one. - Keyboard: Tab reaches each item in turn; Enter or Space opens and closes its panel, and Tab then walks the links of the panel before the next item. Left and Right move along the row, and carry the open panel with them; Home and End go to its ends. Down enters the panel, Up and Down walk it, Up from the first link goes back to the item. Escape closes and puts the focus back on the item. Tabbing out of the panel or of the menu closes it; the focus is never held.
- One setting for every menu of the site, without opening the file: declare
window.MegaMenuSettings = { morph: 0.7 }in a script placed beforemega-menu.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-mega-menu-*attribute on the header itself (data-mega-menu-morph="0.7",data-mega-menu-hover-delay="0.15"). - What the script writes: on the header,
data-mega-menu-mode,desktoporphone,data-mega-menu-state,closed,opening,openorclosing, and--mega-menu-accent. On the item whose panel shows,data-mega-menu-active, anddata-mega-menu-leavingon the one it replaces while its content goes. On a trigger,role,aria-expandedandaria-controls, with anidon a panel that had none. While a panel is open, the place of that panel and the place and size of the surface, inline; all of it is taken off when the menu closes. - From JavaScript:
MegaMenu.mount(element)returns the instance, withopen(index),close(),toggle(index),isOpen(),index(), the item whose panel shows or -1,mode(),refresh()after a layout change the engine cannot hear, anddestroy(). The header emitsmegamenu:open,megamenu:changeeach time the panel shown changes, andmegamenu:close. - **
prefers-reduced-motion**: every state is reached at once, the panel, the sheet and the accordions alike. So is it when GSAP is not on the page. - The words and the pictures of the markup are the preview's, and they ship as they are. Put your own in their place.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- Structure: the header carries
-
Avoid the pitfalls
- The items must not be positioned. Every panel is placed against the header, so nothing between the header and a panel may be
position: relative, nor carry atransformor afilter. The stylesheet takes the position off the items; a wrapper of your own is yours to check. - The header needs room on its sides. A panel is kept inside the side paddings of the header. A header with no padding lets the panel touch the edge of the window.
- A panel wider than the window is not shrunk. Give its class a width that follows the window,
min(920px, calc(100vw - 40px))for one. - Nothing above the header may cut it. An ancestor with
overflow: hiddenand the height of the bar cuts the panel off. The header also needs az-indexover the page it opens on; the stylesheet proposes 40. - The phone sheet hangs from the foot of the header to the foot of the window. The header has to be at the top of the window when the button is pressed: fixed, sticky, or a page that has not scrolled. Its background has to be opaque, or the page shows through the bar above the sheet.
- On a phone the content block carries no vertical padding. It is what folds, and a padding does not fold: a closed accordion would keep it as a gap. The look of the preview puts that air on the labels and under the picture instead.
- The breakpoint lives in two places.
breakpointswitches the behaviour; the phone look of the preview is a@media (max-width: 767px)block at the end of the stylesheet, and in Webflow the mobile landscape breakpoint of each class. Move one and move the other. - A trigger that has to stay a link cannot also open a panel on a click. Put the link to the section's page inside the panel, as its first link.
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a header that already carries the menu returns the running one. A page-wide block goes inwindow.MegaMenuSettings, whichauto()reads itself. - Content that changes the size of an open panel is followed: the surface snaps to the new size. A change of the header's own layout that comes with no resize needs
refresh(). - In Webflow the phone button is a Link block, since the Designer has no button element that holds children: it keeps
role="button", and the paste carries the few lines that let Space press it.
- The items must not be positioned. Every panel is placed against the header, so nothing between the header and a panel may be
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Mega menu is an animated navigation component for Webflow and vanilla JavaScript, built on GSAP 3. Also called morphing mega menu, mega dropdown navigation or morphing dropdown menu.
A site header whose top-level items open a wide panel. The panel is one single surface: it unfolds under the item, then morphs, size and place, from one item to the next instead of closing and opening again. What it holds crosses with a short slide in the direction of the move, the links come up one after the other and the featured picture is uncovered from the side the panel arrives from. It opens under a pointer that rests on an item, with intent: a pointer that crosses another item on its way down to the panel does not switch it. A click or a tap opens it too and pins it. The whole menu works from the keyboard: Tab walks the items and the open panel in order, the arrow keys move along the row and through a panel, Escape closes and hands the focus back to the item. The triggers carry aria-expanded and aria-controls, nothing traps the focus, and a click outside closes. Under the phone breakpoint the row becomes a sheet under the bar that fills the window, and the same panels fold as accordions. Under reduced motion every state is reached at once. Without the script the items are plain links to their own pages, and a panel still shows under the pointer or the keyboard. Not the curtain menu, which covers the page, and not the anchored menu, which is one box that unfolds: here the page stays, and it is the panel that travels along the header. Pattern type mega menu, mega dropdown, morphing navigation.
Updated
Questions
Put data-mega-menu on the header, data-mega-menu-item on each entry of its row, and inside an entry its trigger, data-mega-menu-trigger, then its panel, data-mega-menu-panel, with one block marked data-mega-menu-content around what it shows. Give each panel a width in its own class: the engine only decides where it goes. One surface behind every panel travels and changes size from item to item, so the menu never closes and opens again.
GSAP 3 drives the motion, with no plugin, and the engine reads it at each interaction, so the tags can come in any order and Webflow's own copy is used when it is there. Without GSAP the menu still opens, switches panel and closes, at once and with no motion.
Yes. Tab reaches each item, Enter or Space opens and closes its panel, Left and Right move along the row and carry the open panel with them, Down enters the panel and Escape closes it and puts the focus back on the item. The triggers carry aria-expanded and aria-controls, written by the engine, and the focus is never held.
It already waits. A panel opens after the pointer has rested on an item for data-mega-menu-hover-delay, 0.09 s by default, and an item crossed on the way down to the open panel waits longer on its own. data-mega-menu-close-delay, 0.22 s by default, is how long the pointer may stay off the menu, which carries it across the gap between the bar and the panel.
At or under data-mega-menu-breakpoint, 767 px by default, the row becomes a sheet under the bar that fills the rest of the window, opened by the button marked data-mega-menu-toggle. The same panels fold under their triggers as accordions, one open at a time, and the page behind is held while the sheet is open.