ui-002

Animated Mega Menu

£10 or the whole Vault, £99

An animated mega menu for site headers: the flyout panel morphs between section sizes instead of popping, and its columns stagger in behind it.

intermediate
10 more details
micro-interactionmega-menumorphinghover-effectstaggerkeyboard-navigationfocus-managementmatchMediareduced-motionresponsive-layout

This element reads better at your own screen size than in the frame below:

Updated

About this component

Header navigation whose flyout panel morphs between section sizes instead of popping, with the panel's columns staggering in behind the morph. Hover intent and a diagonal grace path stop it switching on the way to a panel, the keyboard follows the disclosure navigation pattern, and below a breakpoint the same markup becomes a menu button and a full-width sheet of accordion sections. Mounted with MegaMenu.mount(nav, options), with open, close and destroy methods and open and close events. Vanilla JavaScript and GSAP core, no framework, in light and dark themes switched by a single data-variant attribute.

Animated Mega Menu - UI component preview

What's included

8 items
  • Panel width and height morph between sections with a measured tween, no plugin
  • Each section's columns stagger in as the panel lands
  • Hover intent with a diagonal grace path, so crossing a trigger on the way to the panel does not switch it
  • Disclosure navigation keyboard model: Enter, Space and arrows open, arrows move along the bar, Escape returns focus
  • Below a breakpoint: a menu button and a full-width sheet of accordion sections, with focus trap and scroll lock
  • Reduced motion swaps panels instantly
  • MegaMenu.mount API with open, close, destroy and megamenu:open / megamenu:close events
  • Light and dark themes, every token defined in both

Perfect for

3 use cases
  • Product and marketing sites with grouped, multi-column navigation that has to work on phones
  • Web apps with a feature menu in the top bar
  • Documentation sites with sectioned resource menus

How it works

3 sections

Measured morph

When the section changes on an open panel, the script reads the box's current width and height, swaps the section, reads the new natural size and tweens width and height between the two. Dimensions tween, never scale, so the text inside never squashes.

Hover intent and grace path

A section opens after a short rest on its trigger. While one is open, a pointer moving down into the triangle between its recent position and the panel's top edge holds the current section instead of switching to a trigger it crosses.

One markup, two layouts

gsap.matchMedia runs complementary layout and reduced-motion pairs. Below the breakpoint the triggers, their sections and the links move into a generated sheet where each section is an accordion; crossing back or calling destroy() puts every node where it was.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Animated Mega Menu opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, every colour and radius a custom property

  • assets/script.js: readable, commented source exposing one global, MegaMenu

  • The panel's width and height morph between sections: a measured tween of the real dimensions, no plugin

  • Each section's columns stagger in as the panel lands

  • Hover intent: a section opens after a short rest on its trigger, and a grace path keeps the open section while the pointer travels diagonally down to its panel across another trigger

  • Disclosure navigation from the keyboard: Enter, Space and Down arrow open, Left and Right move across the bar, Escape closes and returns focus

  • Below a breakpoint the bar collapses to a menu button that opens a full-width sheet; every section becomes an accordion with a measured height tween and a rotating chevron, built from the same markup, not a second copy

  • The sheet keeps the overlay contract: Escape, focus held inside, focus returned to the menu button, the page behind locked

  • Reduced motion: panels swap instantly and opens become a short fade

  • A drop-in API: MegaMenu.mount(), open(), close(), destroy(), and megamenu:open / megamenu:close events; any number of menus per page

Quick Start

Code snippet omitted: it ships with the download.

Copy the .nav-mega-container markup from index.html into your page. The markup the script depends on:

  • button.mega-toggle: the menu button shown below the breakpoint. Optional: if it is missing the script creates a plain one labelled "Menu" and removes it again on destroy()
  • button.mega-trigger with data-panel="<name>" and aria-controls="<panel id>": one per section
  • .mega-flyout-wrapper > .mega-flyout-box: holds the sections
  • .mega-content-section with the id the trigger's aria-controls names: the matching panel
  • .mega-card: every link inside a panel, including the feature card; group them into .mega-col columns, because a panel's direct children are what stagger in
  • a.mega-link, a.mega-cta, .mega-spacer: plain top-level links, the primary action, and the flexible gap

Ids are only used to pair a trigger with its panel, and they are looked up inside the nav, so two menus on one page never cross.

Upgrading from the earlier version. It found its elements by fixed ids and needed the Flip plugin. Markup that still has id="megaNav" is mounted automatically on load with the defaults, so an existing page keeps working with no script tag changes; the Flip script tag can be deleted. Two behaviours changed on purpose: focusing a trigger no longer opens it (Enter, Space or Down arrow do, as the disclosure pattern expects), and below 768px the bar becomes the menu button and sheet. Pass a smaller breakpoint to keep the bar longer.

Demo furniture. The toolbar strip across the top of index.html and the .stage wrapper around the bar are the demo's ground, not part of the component; neither ships in assets/.

API

MegaMenu.mount(navEl, options)

Mounts one menu and returns its instance. Mounting the same element twice returns the existing instance.

Option Default What it does
breakpoint 768 Viewport width in px below which the bar becomes a menu button and sheet
hoverDelay 90 How long, in ms, the pointer rests on a trigger before its section opens or switches
closeDelay 180 Grace period, in ms, after the pointer leaves the nav before the panel closes
keepInBar '.mega-cta' Selector for top-level items that stay in the bar beside the menu button on a phone; everything else moves into the sheet
toggleLabel 'Menu' Text for a menu button the script has to create

Instance

  • open(section): opens that section (the data-panel name). On the bar it morphs to it; on a phone it opens the sheet with that section expanded. With no argument the bar opens its first section and the sheet opens with nothing expanded
  • close(): closes the panel or the sheet at once
  • current(): the open section's name, '' for an open sheet with nothing expanded, null when closed
  • layout(): 'bar' or 'sheet'
  • destroy(): reverts everything the mount did, whichever layout is live: listeners removed, inline styles cleared, nodes moved into the sheet put back in their original places, the created menu button removed, the scroll lock released

MegaMenu.get(navEl) returns the instance mounted on an element, or null.

Events

Dispatched on the nav, bubbling:

  • megamenu:open: detail: { section, layout } (the bar also sends previous). Fires when the menu opens and whenever the shown section changes, including an accordion section expanding in the sheet
  • megamenu:close: detail: { section, layout }. Fires when the panel or the sheet closes, including a close forced by the viewport crossing the breakpoint

Code snippet omitted: it ships with the download.

How It Works

Measured morph. When the section changes on an open panel, the script reads the box's current width and height (mid-tween if a previous morph is still running), swaps the active section, reads the box's natural size with the new one in place, and tweens width and height from the first pair to the second, then clears them. Each section sets its own width (--wide, --mid, --narrow), so the box travels in both axes. Dimensions tween, never scale, so the text inside never squashes, and nothing on the box carries a min-height or it could not shrink.

Hover intent and the grace path. A section opens only after the pointer rests on its trigger for hoverDelay. While a section is open, the script keeps a short history of pointer positions. If the pointer enters another trigger while travelling downward inside the triangle between where it was a moment ago and the panel's top edge, the switch waits and checks again, for up to 600ms, and reaching the panel cancels it. A pointer that stops on the other trigger switches as normal.

Keyboard routing. The flyout markup sits after all the triggers, so an unassisted Tab would walk past them into the last panel. The script sends Tab from an open trigger into its panel, Tab off the last link to the next top-level item, and Shift+Tab off the first link back to the owning trigger.

Two layouts, one markup. gsap.matchMedia runs the menu in one of two layouts from a complementary pair of queries (below and at or above breakpoint), crossed with a complementary pair for reduced motion, so every device matches exactly one combination. Below the breakpoint the script creates a .mega-sheet element and moves the triggers, each trigger's section and the top-level links into it, in order; crossing back, or destroy(), moves every node back to where it was. Before the script runs, the bar squeezes onto a narrow screen as a no-JS fallback.

Accordion height. Expanding a section in the sheet reads its natural height and tweens height and vertical padding up from zero, with the columns staggering in; collapsing tweens them back to zero and then sets hidden, so collapsed links are out of the tab order. One section is open at a time.

Hover geometry. The visual gap between the bar and the panel is padding-top on the flyout wrapper, not a top offset. The pointer crossing that gap is still inside the nav, so the menu does not close under the cursor.

Keyboard and ARIA

The bar follows the disclosure navigation pattern, not role="menu": the links are ordinary links and each trigger is a disclosure button.

  • Each trigger is a <button> with aria-expanded and aria-controls pointing at its panel
  • Enter or Space on a trigger: opens its section, or closes it if it is already open; focus stays on the trigger
  • Down arrow on a trigger: opens its section and moves focus to its first link (Up arrow: the last link)
  • Left / Right arrow on a top-level item: moves focus along the bar, wrapping; Home / End jump to the first and last. If a section is open, landing on another trigger switches to it and landing on a plain link closes it
  • Down / Up arrow inside a panel: next and previous link; Up from the first link returns to the trigger
  • Tab on an open trigger: into its panel; Tab off the last link: the next top-level item; Shift+Tab off the first link: back to the owning trigger
  • Escape: closes the panel and returns focus to its trigger
  • Focus leaving the nav, or a press outside it, closes the panel; focus inside it outranks the pointer leaving

In the sheet:

  • The menu button carries aria-expanded and aria-controls pointing at the sheet
  • Opening moves focus to the first row; Tab and Shift+Tab cycle through the menu button and the sheet and never leave it
  • Each section row is a disclosure button (aria-expanded); Enter or Space toggles it, Down arrow expands it and moves into its links
  • Escape, a press outside the sheet, or following a link closes it; Escape and the menu button return focus to the menu button
  • The page behind is locked on both <html> and <body> while the sheet is open, and the scroll position restored on close

Themes

One design at two token values, set with data-variant on <body> in the demo, or on the component root in your own page:

Code snippet omitted: it ships with the download.

  • light: grey ground, ink text (the default)
  • dark: the deep ground, chalk text

Both blocks sit at the top of assets/style.css and define every value the component uses: --ground, --ground-2, --raised, --hover, --ink, --ink-2, --ink-3, --line, --line-strong, --accent, --accent-ink, --shadow, --shadow-lg, --font and the three radii. The menu button and the sheet use the same tokens. Nothing in assets/script.js reads the theme name, so re-valuing those two blocks is the whole of a rebrand: set --accent to your own brand colour and --accent-ink to something that clears 4.5:1 on it.

index.html?variant=dark loads with one preselected; the demo toolbar just sets the same attribute.

Customisation

  • Panel widths are the .mega-content-section--wide / --mid / --narrow rules; add a size the same way and the morph picks it up with no script change.
  • The phone breakpoint, hover delay and close grace are mount() options.
  • Easing and durations are the gsap.fromTo / gsap.to calls in show, hide, expand, collapse and openSheet in assets/script.js.
  • The gap under the bar is padding-top on .mega-flyout-wrapper; change the number, never move it to top.
  • The sheet starts at the bottom of the bar (the script writes --mega-sheet-top on the nav when it opens) and runs to the bottom of the viewport; its padding and row heights are the .mega-sheet rules.

Requirements

  • GSAP 3.12+ (core only; no plugins)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.