Anchored expandable menu
Navigation anchored at the top of the page, compact at rest: a small floating rectangle, centered (logo, accent dot, "[ Menu ]" button) in frosted glass.
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.Copies the version "Reference version". Pick another version above the preview to copy that one. -
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 tutorial -
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-anchored-menu Marks the section the effect plays on. It takes no value: leave the value field empty. data-anchored-menu-toggle Marks the button that opens and closes the menu. It can sit outside the menu, in a bar of its own. Without it nothing opens the menu but JavaScript, and the console says nothing. [ Required ] data-anchored-menu-panel Marks the block that unfolds. Without it the engine takes the last block inside the menu, and refuses to mount, with a warning in the console, when the menu holds a single block. [ Optional ] data-anchored-menu-links Marks the box holding the links that rise one after the other. Without it the engine takes the first nav inside the menu, and moves no link when there is none. [ Optional ] data-anchored-menu-link Marks one link to move, for links that do not all sit in the same box. As soon as one link carries it, only the marked links rise. [ Optional ] data-anchored-menu-mask Marks the box that clips a row while its link rises. Without it the engine wraps each link in a box it builds, and the hairline you drew between two rows is not on it. [ Optional ] data-anchored-menu-block Marks a side block that settles after the links, a contact card or a footer line. Without it the block is simply there from the start. [ 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-anchored-menu-widen 0.45 How long the box takes to stretch from its compact width to the width of the panel, in seconds. data-anchored-menu-widen-ease power3.inOut Shape of the widening, where it speeds up and where it settles. Options: any GSAP ease, power2.out and expo.inOut for instance. data-anchored-menu-grow 0.5 How long the box takes to drop down to the height of the panel, in seconds. data-anchored-menu-grow-ease power3.inOut Shape of the drop in height. Options: any GSAP ease, power2.out and expo.inOut for instance. data-anchored-menu-grow-overlap 0.12 How early the height starts, counted back from the end of the widening, in seconds. data-anchored-menu-reveal 0.6 How long one link takes to rise out of its row, in seconds. data-anchored-menu-reveal-stagger 0.05 Delay between two links rising, in seconds. data-anchored-menu-reveal-ease power4.out Shape of the links rising. Options: any GSAP ease, back.out and power2.out for instance. data-anchored-menu-reveal-from 130 How far under its row a link starts out, as a percentage of a row height. data-anchored-menu-reveal-overlap 0.2 How early the links start, counted back from the end of the drop in height, in seconds. data-anchored-menu-lift clamp(10px, 1.55vh, 18px) How far a side block travels up while it fades in, the coordinates card and the foot line, any CSS unit. data-anchored-menu-blocks 0.4 How long one side block takes to settle in, in seconds. data-anchored-menu-blocks-stagger 0.06 Delay between two side blocks, in seconds. data-anchored-menu-blocks-ease power2.out Shape of the side blocks settling in. Options: any GSAP ease, back.out and power2.out for instance. data-anchored-menu-blocks-offset 0.05 Delay between the links starting to rise and the side blocks starting to fade, in seconds. data-anchored-menu-open-speed 1 Playback speed of the whole unfolding, 1 being the durations above. data-anchored-menu-close-speed 1.35 Playback speed of the folding back, which plays the same timeline backwards. data-anchored-menu-escape true Whether the Escape key closes the menu. Options: true, false. data-anchored-menu-close-on-outside true Whether a click anywhere outside the box closes the menu. Options: true, false. data-anchored-menu-close-on-link true Whether a click on a menu link closes the menu. Options: true, false, false being for links that open elsewhere. data-anchored-menu-remeasure 120 Delay before a box caught mid travel by a resize or a late face reads both sizes again, in ms. At rest nothing is read, the CSS sizes the box. data-anchored-menu-label-open [ Menu ] Text written in the button while the menu is closed. Options: any short phrase; a button holding an icon is left alone. data-anchored-menu-label-close [ Close ] Text written in the button while the menu is open. Options: any short phrase; a button holding an icon is left alone. data-anchored-menu-debug false Whether the engine writes what it read on the menu element, as data-anchored-menu-debug-* attributes (state, last event, size at the click, the two measured sizes, current size, inline size, GSAP version, mount time), and logs every change in the console. Options: true, false. On for a preview you cannot inspect, off on a live site.
Two files, no build step. It also needs GSAP 3 on the page for the travel, loaded with the tag in step 1. Without it the menu still opens and closes, at once and with no travel.
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
AnchoredMenu.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-anchored-menu-widen 0.45 How long the box takes to stretch from its compact width to the width of the panel, in seconds. It snaps open sideways A slow, deliberate stretch data-anchored-menu-widen-ease power3.inOut Shape of the widening, where it speeds up and where it settles. Options: any GSAP ease, power2.out and expo.inOut for instance. data-anchored-menu-grow 0.5 How long the box takes to drop down to the height of the panel, in seconds. The panel is there in one go The box unrolls slowly downwards data-anchored-menu-grow-ease power3.inOut Shape of the drop in height. Options: any GSAP ease, power2.out and expo.inOut for instance. data-anchored-menu-grow-overlap 0.12 How early the height starts, counted back from the end of the widening, in seconds. Two separate stages, width then height One single unfolding, the two stages blur together data-anchored-menu-reveal 0.6 How long one link takes to rise out of its row, in seconds. The rows snap into place A long, drawn-out rise data-anchored-menu-reveal-stagger 0.05 Delay between two links rising, in seconds. The list arrives as one block The rows come in one by one data-anchored-menu-reveal-ease power4.out Shape of the links rising. Options: any GSAP ease, back.out and power2.out for instance. data-anchored-menu-reveal-from 130 How far under its row a link starts out, as a percentage of a row height. It barely slides It comes from further below, and starts later inside its own row data-anchored-menu-reveal-overlap 0.2 How early the links start, counted back from the end of the drop in height, in seconds. The rows wait for the box They rise while the box is still growing data-anchored-menu-lift clamp(10px, 1.55vh, 18px) How far a side block travels up while it fades in, the coordinates card and the foot line, any CSS unit. It barely moves It slides in from further down data-anchored-menu-blocks 0.4 How long one side block takes to settle in, in seconds. They are simply there They drift into place data-anchored-menu-blocks-stagger 0.06 Delay between two side blocks, in seconds. They arrive together One after the other, down the panel data-anchored-menu-blocks-ease power2.out Shape of the side blocks settling in. Options: any GSAP ease, back.out and power2.out for instance. data-anchored-menu-blocks-offset 0.05 Delay between the links starting to rise and the side blocks starting to fade, in seconds. Rows and card arrive together The card lands well after the rows data-anchored-menu-open-speed 1 Playback speed of the whole unfolding, 1 being the durations above. The menu opens slower It opens faster data-anchored-menu-close-speed 1.35 Playback speed of the folding back, which plays the same timeline backwards. The menu lingers on the way out It gets out of the way data-anchored-menu-escape true Whether the Escape key closes the menu. Options: true, false. data-anchored-menu-close-on-outside true Whether a click anywhere outside the box closes the menu. Options: true, false. data-anchored-menu-close-on-link true Whether a click on a menu link closes the menu. Options: true, false, false being for links that open elsewhere. data-anchored-menu-remeasure 120 Delay before a box caught mid travel by a resize or a late face reads both sizes again, in ms. At rest nothing is read, the CSS sizes the box. The travel follows a dragged window closely, at the cost of reading more often Fewer readings, and a travel may land on an old size that the rest state then corrects in one step data-anchored-menu-label-open [ Menu ] Text written in the button while the menu is closed. Options: any short phrase; a button holding an icon is left alone. data-anchored-menu-label-close [ Close ] Text written in the button while the menu is open. Options: any short phrase; a button holding an icon is left alone. data-anchored-menu-debug false Whether the engine writes what it read on the menu element, as data-anchored-menu-debug-* attributes (state, last event, size at the click, the two measured sizes, current size, inline size, GSAP version, mount time), and logs every change in the console. Options: true, false. On for a preview you cannot inspect, off on a live site. -
Drive it from JavaScript
When something other than the control has to open or close the menu, or when the page reacts to it.const element = document.querySelector('[data-anchored-menu]'); element.addEventListener('anchoredmenu:open', (e) => { console.log(e.detail); }); const menu = AnchoredMenu.mount(element); // the menu auto() mounted, or a new one menu.open(); // and menu.close(), menu.toggle(), menu.isOpen() // When the menu leaves the page: menu.destroy(); // puts the two states back and drops the listeners
-
Fit it to your page
- One setting for every menu of the page, without opening the file: declare
window.AnchoredMenuSettings = { widen: 0.6 }in a script placed beforeanchored-menu.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-anchored-menu-*attribute on the menu element, so one menu can still differ from the page-wide setting. - GSAP is read at each click, not at mount. The menu mounts on DOM ready whether the library is there or not, so GSAP, Webflow's own copy included, can land after the script, and the engine never loads a second one on top. A box that is not rendered at the click, inside a
display: noneparent for one, also switches state at once, with no travel. - Minimal structure, as in
snippet.html: a block carryingdata-anchored-menu, holding a bar with a button carryingdata-anchored-menu-toggleand a panel carryingdata-anchored-menu-panel. Inside the panel, a nav (data-anchored-menu-links), one mask per row (data-anchored-menu-mask) and the blocks that fade in (data-anchored-menu-block). - The script builds nothing except the masks, and only for links that do not already sit in one. Everything else is markup you own, so a builder can style it. The masks are worth keeping in the markup rather than letting the script wrap the links: the hairline between two rows is carried by the mask, and a mask you wrote is a box you can style.
- The rest is styled, not tuned: the width of the panel, so the width of the open box, the padding of the bar, so the height of the compact box, the size, weight and colour of a link, the hairline between two rows, and the background, glass, border and radius of the box are your own classes. The surface of the box when no class sets one is the custom property
--anchored-menu-surface.anchored-menu.csscarries the mechanics only: the clip, the folded panel, the row masks. Everything it proposes and does not need (the fixed position at the top, the glass, the border, the 14 px radius, the z-index) sits inside:where(), which carries no specificity: one class of yours wins over it, with no!importantanywhere. - The text of the button is a setting, not markup: the engine writes
labelOpenandlabelCloseinto it, so words written in the markup are replaced. A button that holds elements rather than plain text holds an icon, and the engine leaves its markup alone. - The panel never covers the page: no overlay, no scroll lock, no focus trap. If the site needs a darkened backdrop, add it as a separate layer and animate it on
anchoredmenu:openandanchoredmenu:close. - **From JavaScript, beyond
open,close,toggle,isOpenanddestroy**: the api ofAnchoredMenu.mount(element, overrides)also carriessettings,timeline(), the travel under way andnullat rest, andmeasure(), which reads both sizes again during a travel, for a layout change the engine cannot hear; at rest there is nothing to read, the CSS already sizes the box. The element firesanchoredmenu:closeas well asanchoredmenu:open, both bubbling, with{ menu }asevent.detail.mount()returnsnull, with a warning in the console, when no block carriesdata-anchored-menu-panel, andAnchoredMenu.auto(container)mounts every menu found inside a container. - Debugging a preview you cannot inspect, the Webflow Designer's for one:
data-anchored-menu-debug="true"on the menu writes the last readings on the element. - The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own content in their place whenever they want, in the page's language. The demos also put a
preventDefaulton their nav links: remove it. - 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.
- One setting for every menu of the page, without opening the file: declare
-
Avoid the pitfalls
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts at DOM ready, andmount()on an element that already carries a menu hands back the existing one untouched. A page-wide block therefore goes inwindow.AnchoredMenuSettings, whichauto()reads itself; handed to a latermount()call it is silently dropped, and the settings block ends up looking decorative. - Never give the box a width or a height, in CSS or from a script. At rest only the CSS decides its size: closed it hugs the bar, open it hugs the panel. The engine reads the two sizes at the click, writes sizes during the travel and removes every one of them when it lands. Put the open width on the panel, for example
.panel { width: min(600px, calc(100vw - 40px)) }: a width on[data-anchored-menu]gives both states the same width, and the menu only drops down. - **
overflow: hiddenon the menu is what hides the panel at rest**: do not remove it to hang a drop shadow on the panel, everything would show. - The hairline between two rows sits on the mask, never on the link: the link moves inside its mask during the reveal, so a hairline carried by the link would ride up with it.
- Keep the radius of the box a fixed length. A radius that stayed proportional to the box would open the corner as the box grows, which reads as a shape morphing into another one. A constant corner is what makes the two stages read as one rectangle. Hairlines stay at 1 px for the same kind of reason: a hairline that scales is no longer a hairline.
- **
backdrop-filterdoes not apply if an ancestor carries afilteror some kinds ofoverflow**: on a real site, keep the menu a direct child ofbody. - A button outside the menu opens it while the page holds a single menu. With several, keep each button inside its own menu, as
snippet.htmldoes, or give itaria-controlswith the id of the menu, or keep it in the samesectionas the menu. A menu left without a button says so in the console.
- **Settings handed to
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
Anchored expandable menu is an animated navigation component for Webflow and vanilla JavaScript, built on GSAP 3. Also called expanding floating navbar, floating navbar or dynamic island menu.
Navigation anchored at the top of the page, compact at rest: a small floating rectangle, centered (logo, accent dot, "[ Menu ]" button) in frosted glass. On click it unfolds into a panel without ever covering the page, in two crisp stages: the width stretches first, then the height drops down, the radius staying constant so the box reads as one rectangle that grows instead of a shape that morphs. The links then rise out of their mask in the left-hand column, the coordinates (mono // labels) settle into a recessed card on the right, and a system line plus the copyright close the panel. The "[ Close ]" button sits at the right edge of the expanded panel and has no effect on the compact state. Folding back is an accelerated rewind, on the button, on a click outside or on Escape. Both states are measured at the click, never guessed, and never frozen: at rest the box is sized by the CSS alone, whatever loaded after the page. Same art direction as the curtain-menu component: ink on cream, the ink #15100c as the one tint (dot, hovers, OK words, selection), terminal layer, scramble effect. Pattern type: expandable floating nav.
Updated
Questions
Paste the component, then tune it with custom attributes on the box marked data-anchored-menu, for instance data-anchored-menu-widen="0.6". The glass, the border, the radius and the width of the open panel are your own classes, and the engine never reads a class.
Yes, the GSAP 3 core with no plugins, and the embed loads 3.12.5. GSAP is read at each click, not at mount, so Webflow's own copy can land after the script and no second copy is loaded. Without GSAP the menu still opens: the state switches at once and the CSS sizes the box.
Use data-anchored-menu-widen for the sideways stretch, 0.45 seconds by default, and data-anchored-menu-grow for the drop in height, 0.5 seconds. Lower values snap the box open, higher ones make a slow, deliberate stretch. data-anchored-menu-open-speed scales the whole unfolding, and closing plays the same timeline backwards at 1.35 times the speed.
No. There is no overlay, no scroll lock and no focus trap: the panel unfolds from the bar and the page stays usable. It folds back on the button, on a click outside, on Escape or on a link click, and the last three can each be turned off, for instance data-anchored-menu-close-on-outside="false".
Because the box itself was given a width. A width on [data-anchored-menu] gives the closed and open states the same width, so only the height moves. Put the open width on the panel marked data-anchored-menu-panel and leave the box unsized.