Morphing Menu Button

A single element that transforms from a compact MENU button into a full navigation panel. Built with CSS + GSAP.

← Click the red MENU button

Implementation Guide

1. CDN Script

Add GSAP before your closing </body> tag:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>

2. Required CSS

All selectors use data-menu attributes, making them portable to any builder (Webstudio, Webflow, etc.).

:root {
  --menu-red: #e30613;
  --menu-off-white: #edebe7;
  --menu-black: #111;
}

/* Container */
[data-menu="root"] {
  position: fixed;
  top: 28px;
  right: 32px;
  z-index: 100;
  width: 12ch;
  border: 3px solid var(--menu-red);
  background: var(--menu-red);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: background 0.3s ease, border-color 0.3s ease;
}

/* Toggle button */
[data-menu="toggle"] {
  display: inline-flex;
  align-items: stretch;
  cursor: pointer;
  border: none;
  background: transparent;
  padding: 0;
  outline: none;
  flex-shrink: 0;
}

/* Hamburger icon box */
[data-menu="icon"] {
  width: 42px;
  height: 42px;
  background: var(--menu-red);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
}

/* Hamburger lines */
[data-menu="line"] {
  display: block;
  width: 20px;
  height: 2px;
  background: #fff;
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Label text */
[data-menu="label"] {
  display: flex;
  align-items: center;
  padding: 0 16px;
  font-family: "Anton", sans-serif;
  font-size: 15px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #fff;
  white-space: nowrap;
  transition: color 0.3s ease;
}

/* Closed hover: invert colors */
[data-menu="root"]:not(.is-open):hover {
  background: #fff;
  border-color: var(--menu-red);
}
[data-menu="root"]:not(.is-open):hover [data-menu="label"] {
  color: var(--menu-red);
}

/* Open hover: minus line rotates */
[data-menu="root"].is-open:hover [data-menu="line"] {
  transform: rotate(-45deg);
}

/* Expandable body */
[data-menu="body"] {
  height: 0;
  overflow: hidden;
  background: var(--menu-off-white);
  display: flex;
  flex-direction: column;
}

/* Nav links */
[data-menu="nav"] {
  padding: 32px 40px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

[data-menu="link"] {
  display: block;
  font-family: "Anton", sans-serif;
  font-size: clamp(50px, 9vw, 78px);
  color: var(--menu-black);
  text-decoration: none;
  text-transform: uppercase;
  line-height: 1.05;
  letter-spacing: -1px;
  overflow: hidden;
  transition: color 0.25s ease;
}

[data-menu="link"] span { display: inline-block; }
[data-menu="link"]:hover { color: var(--menu-red); }

3. HTML Structure

Build this element tree. Each data-menu value tells the JS which role the element plays.

Box data-menu="root"
├─ Button data-menu="toggle"
│   ├─ Box data-menu="icon"
│   │   ├─ Span data-menu="line"
│   │   ├─ Span data-menu="line"
│   │   └─ Span data-menu="line"
│   └─ Span data-menu="label" → "Menu"
└─ Box data-menu="body"
    └─ Nav data-menu="nav"
        ├─ Link data-menu="link" → <span>Home</span>
        ├─ Link data-menu="link" → <span>Services</span>
        ├─ Link data-menu="link" → <span>Project</span>
        ├─ Link data-menu="link" → <span>About</span>
        └─ Link data-menu="link" → <span>Contact</span>
<div data-menu="root">
  <button data-menu="toggle">
    <div data-menu="icon">
      <span data-menu="line"></span>
      <span data-menu="line"></span>
      <span data-menu="line"></span>
    </div>
    <span data-menu="label">Menu</span>
  </button>

  <div data-menu="body">
    <nav data-menu="nav">
      <a href="#" data-menu="link"><span>Home</span></a>
      <a href="#" data-menu="link"><span>Services</span></a>
      <a href="#" data-menu="link"><span>Project</span></a>
      <a href="#" data-menu="link"><span>About</span></a>
      <a href="#" data-menu="link"><span>Contact</span></a>
    </nav>
  </div>
</div>

4. JavaScript

Paste this after the GSAP CDN script. The JS is fully self-contained and queries elements by data-menu attributes.

document.addEventListener("DOMContentLoaded", function () {
  var root = document.querySelector('[data-menu="root"]');
  var toggle = document.querySelector('[data-menu="toggle"]');
  var label = document.querySelector('[data-menu="label"]');
  var icon = document.querySelector('[data-menu="icon"]');
  var lines = document.querySelectorAll('[data-menu="line"]');
  var menuBody = document.querySelector('[data-menu="body"]');
  var navSpans = document.querySelectorAll('[data-menu="link"] span');

  if (!root || !toggle) return;

  var line1 = lines[0];
  var line3 = lines[2];
  var isOpen = false;
  var openTl, closeTl;
  var closedWidth = root.offsetWidth;

  function openMenu() {
    if (isOpen) return;
    isOpen = true;
    root.classList.add("is-open");
    if (closeTl) closeTl.kill();

    openTl = gsap.timeline({ defaults: { ease: "power3.out" } });

    openTl
      .to(root, {
        borderWidth: 3,
        width: Math.min(480, window.innerWidth * 0.92),
        background: "#fff",
        duration: 0.45,
        ease: "power3.inOut",
      })
      .to([line1, line3], { opacity: 0, duration: 0.25, ease: "power2.in" }, 0)
      .to(icon, { gap: 0, duration: 0.25 }, 0)
      .to(label, { color: "#e30613", duration: 0.3 }, 0.1)
      .add(function () { label.textContent = "Close"; }, 0.15)
      .to(menuBody, { height: "auto", duration: 0.55, ease: "power3.inOut" }, 0.2)
      .set(toggle, { borderBottom: "2px solid #e30613" }, 0.2)
      .fromTo(
        navSpans,
        { y: 70, opacity: 0 },
        { y: 0, opacity: 1, duration: 0.5, stagger: 0.06, ease: "power3.out" },
        0.4
      );
  }

  function closeMenu() {
    if (!isOpen) return;
    isOpen = false;
    if (openTl) openTl.kill();

    closeTl = gsap.timeline({
      defaults: { ease: "power3.inOut" },
      onComplete: function () {
        root.classList.remove("is-open");
        gsap.set([root, toggle, label, menuBody], { clearProps: "all" });
        gsap.set(navSpans, { clearProps: "all" });
        gsap.set([line1, line3], { clearProps: "all" });
        gsap.set(icon, { clearProps: "all" });
      },
    });

    closeTl
      .to(navSpans, {
        y: -30, opacity: 0, duration: 0.25, stagger: 0.03, ease: "power2.in"
      }, 0)
      .to(menuBody, { height: 0, duration: 0.4, ease: "power3.inOut" }, 0.15)
      .set(toggle, { borderBottom: "none" }, 0.15)
      .to(root, {
        borderWidth: 3, width: closedWidth, background: "#e30613",
        duration: 0.4, ease: "power3.inOut"
      }, 0.2)
      .to(label, { color: "#fff", duration: 0.25 }, 0.25)
      .add(function () { label.textContent = "Menu"; }, 0.3)
      .to(icon, { gap: 5, duration: 0.25 }, 0.25)
      .to([line1, line3], { opacity: 1, duration: 0.25, ease: "power2.out" }, 0.3);
  }

  toggle.addEventListener("click", function () {
    isOpen ? closeMenu() : openMenu();
  });

  document.addEventListener("keydown", function (e) {
    if (e.key === "Escape" && isOpen) closeMenu();
  });
});

5. Data Attributes Reference

Attribute Element Description
data-menu="root" Outer container The fixed-position wrapper. Morphs between button and panel.
data-menu="toggle" Button Clickable trigger. Receives the border-bottom separator when open.
data-menu="icon" Box inside button Contains the 3 hamburger lines. Gap animates to 0 on open.
data-menu="line" 3 × Span Hamburger lines. Lines 1 & 3 fade out, line 2 stays as minus.
data-menu="label" Span Text that swaps between "Menu" and "Close".
data-menu="body" Box Expandable container. Height tweens from 0 to auto.
data-menu="nav" Nav / Box Wraps the navigation links with padding.
data-menu="link" Link (anchor) Each nav item. Must contain a <span> for the stagger animation.

6. Animation Breakdown

The open/close is a single GSAP timeline with these phases:

Time Open Close
0s Container widens, bg → white. Lines 1 & 3 fade out, icon gap → 0 Nav items slide up & fade. Footer fades.
0.1s Label color → red
0.15s Label text swaps to "Close" Body collapses. Separator removed.
0.2s Body expands. Red separator appears. Container shrinks, bg → red.
0.4s Nav items stagger in (y: 70 → 0) Label text → "Menu", lines fade back in.

7. Webstudio Implementation

In Webstudio, add custom attributes via Settings panel → Custom Attributes on each element. The CSS and JS go into separate HTML Embed components.

What Where in Webstudio
Google Font <link> Project Settings → Custom Code → Head
CSS <style> block HTML Embed at top of page (or Head code)
Element tree Visual editor — add data-menu attributes via Settings
GSAP <script> + JS HTML Embed at bottom of page

8. Customization

/* Change the brand color */
:root {
  --menu-red: #your-color;
}

/* Change open panel width (in the JS) */
width: Math.min(480, window.innerWidth * 0.92)  /* ← change 480 */

/* Change closed button width */
[data-menu="root"] {
  width: 12ch;  /* ← change this value */
}

/* Change nav font size */
[data-menu="link"] {
  font-size: clamp(50px, 9vw, 78px);  /* ← adjust clamp values */
}