Scaling Hamburger Navigation

A corner-anchored hamburger navigation where the background pill scales outward from a small button to fill the menu panel. The content group scales from near-zero to full size with the same origin. Active links show a dot indicator; hovering transfers the dot to the hovered item. Entirely CSS-driven.

navigationhamburgercss animationscaleno gsap

Code

HTML
html
<nav data-navigation-status="not-active" class="navigation">
  <div data-navigation-toggle="close" class="navigation__dark-bg"></div>
  <div class="hamburger-nav">
    <div class="hamburger-nav__bg"></div>
    <div class="hamburger-nav__group">
      <p class="hamburger-nav__menu-p">Menu</p>
      <ul class="hamburger-nav__ul">
        <div class="hamburger-nav__li">
          <a href="#" aria-current="page" class="hamburger-nav__a">
            <p class="hamburger-nav__p">Home</p>
            <div class="hamburger-nav__dot"></div>
          </a>
        </div>
        <div class="hamburger-nav__li">
          <a href="#" class="hamburger-nav__a">
            <p class="hamburger-nav__p">Portfolio</p>
            <div class="hamburger-nav__dot"></div>
          </a>
        </div>
        <div class="hamburger-nav__li">
          <a href="#" class="hamburger-nav__a">
            <p class="hamburger-nav__p">Our Expertises</p>
            <div class="hamburger-nav__dot"></div>
          </a>
        </div>
        <div class="hamburger-nav__li">
          <a href="#" class="hamburger-nav__a">
            <p class="hamburger-nav__p">Services</p>
            <div class="hamburger-nav__dot"></div>
          </a>
        </div>
        <div class="hamburger-nav__li">
          <a href="#" class="hamburger-nav__a">
            <p class="hamburger-nav__p">About</p>
            <div class="hamburger-nav__dot"></div>
          </a>
        </div>
        <div class="hamburger-nav__li">
          <a href="#" class="hamburger-nav__a">
            <p class="hamburger-nav__p">Contact</p>
            <div class="hamburger-nav__dot"></div>
          </a>
        </div>
      </ul>
    </div>
    <div data-navigation-toggle="toggle" class="hamburger-nav__toggle">
      <div class="hamburger-nav__toggle-bar"></div>
      <div class="hamburger-nav__toggle-bar"></div>
    </div>
  </div>
</nav>
CSS
css
.navigation {
  z-index: 500;
  pointer-events: none;
  position: fixed;
  inset: 0;
}

.navigation__dark-bg {
  transition: all 0.7s cubic-bezier(0.5, 0.5, 0, 1);
  opacity: 0;
  pointer-events: auto;
  visibility: hidden;
  background-color: #000;
  position: absolute;
  inset: 0;
}

[data-navigation-status="active"] .navigation__dark-bg {
  opacity: 0.33;
  visibility: visible;
}

.hamburger-nav {
  border-radius: 1.5em;
  position: absolute;
  top: 2em;
  right: 2em;
}

.hamburger-nav__bg {
  transition: all 0.7s cubic-bezier(0.5, 0.5, 0, 1);
  background-color: #e2e1df;
  border-radius: 1.75em;
  width: 3.5em;
  height: 3.5em;
  position: absolute;
  top: 0;
  right: 0;
}

[data-navigation-status="active"] .hamburger-nav__bg {
  width: 100%;
  height: 100%;
}

.hamburger-nav__group {
  transition: all 0.5s cubic-bezier(0.5, 0.5, 0, 1), transform 0.7s cubic-bezier(0.5, 0.5, 0, 1);
  grid-column-gap: 1em;
  grid-row-gap: 1em;
  pointer-events: auto;
  transform-origin: 100% 0;
  flex-flow: column;
  padding: 2.25em 2.5em 2em 2em;
  display: flex;
  position: relative;
  transform: scale(0.15) rotate(0.001deg);
  opacity: 0;
  visibility: hidden;
}

[data-navigation-status="active"] .hamburger-nav__group {
  transform: scale(1) rotate(0.001deg);
  opacity: 1;
  visibility: visible;
}

.hamburger-nav__menu-p {
  opacity: .5;
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-bottom: 0;
  font-family: RM Mono, Arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
}

.hamburger-nav__ul {
  grid-column-gap: .375em;
  grid-row-gap: .375em;
  flex-flow: column;
  margin-top: 0;
  margin-bottom: 0;
  padding: 0;
  display: flex;
  position: relative;
}

.hamburger-nav__li {
  margin: 0;
  padding: 0;
  list-style: none;
}

.hamburger-nav__a {
  color: #131313;
  justify-content: space-between;
  align-items: center;
  text-decoration: none;
  display: flex;
}

.hamburger-nav__a[aria-current] .hamburger-nav__p {
  opacity: 0.33;
}

.hamburger-nav__p {
  white-space: nowrap;
  margin-bottom: 0;
  padding-right: 1.25em;
  font-size: 2em;
}

.hamburger-nav__dot {
  transition: all 0.7s cubic-bezier(0.5, 0.5, 0, 1);
  background-color: currentColor;
  border-radius: 50%;
  flex-shrink: 0;
  width: .5em;
  height: .5em;
  transform: scale(0) rotate(0.001deg);
  opacity: 0.5;
}

.hamburger-nav__a[aria-current] .hamburger-nav__dot {
  transform: scale(1) rotate(0.001deg);
  opacity: 1;
}

.hamburger-nav:has(.hamburger-nav__a:hover) .hamburger-nav__dot {
  transform: scale(0) rotate(0.001deg);
}

.hamburger-nav .hamburger-nav__a:hover .hamburger-nav__dot {
  transform: scale(1) rotate(0.001deg);
  opacity: 0.25;
}

.hamburger-nav__toggle {
  transition: transform 0.7s cubic-bezier(0.5, 0.5, 0, 1);
  pointer-events: auto;
  cursor: pointer;
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 3.5em;
  height: 3.5em;
  display: flex;
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(0em, 0em) rotate(0.001deg);
}

[data-navigation-status="active"] .hamburger-nav__toggle {
  transform: translate(-1em, 1em) rotate(0.001deg);
}

.hamburger-nav__toggle-bar {
  transition: transform 0.7s cubic-bezier(0.5, 0.5, 0, 1);
  background-color: #131313;
  width: 40%;
  height: .125em;
  position: absolute;
  transform: translateY(-0.15em) rotate(0.001deg);
}

.hamburger-nav__toggle:hover .hamburger-nav__toggle-bar {
  transform: translateY(0.15em) rotate(0.001deg);
}

[data-navigation-status="active"] .hamburger-nav__toggle .hamburger-nav__toggle-bar {
  transform: translateY(0em) rotate(45deg);
}

.hamburger-nav__toggle .hamburger-nav__toggle-bar:nth-child(2) {
  transform: translateY(0.15em) rotate(0.001deg);
}

.hamburger-nav__toggle:hover .hamburger-nav__toggle-bar:nth-child(2) {
  transform: translateY(-0.15em) rotate(0.001deg);
}

[data-navigation-status="active"] .hamburger-nav__toggle .hamburger-nav__toggle-bar:nth-child(2) {
  transform: translateY(0em) rotate(-45deg);
}
JavaScript
javascript
function initScalingHamburgerNavigation() {
  // Toggle Navigation
  document.querySelectorAll('[data-navigation-toggle="toggle"]').forEach(toggleBtn => {
    toggleBtn.addEventListener('click', () => {
      const navStatusEl = document.querySelector('[data-navigation-status]');
      if (!navStatusEl) return;
      if (navStatusEl.getAttribute('data-navigation-status') === 'not-active') {
        navStatusEl.setAttribute('data-navigation-status', 'active');
        // If you use Lenis you can 'stop' Lenis here: Example Lenis.stop();
      } else {
        navStatusEl.setAttribute('data-navigation-status', 'not-active');
        // If you use Lenis you can 'start' Lenis here: Example Lenis.start();
      }
    });
  });

  // Close Navigation
  document.querySelectorAll('[data-navigation-toggle="close"]').forEach(closeBtn => {
    closeBtn.addEventListener('click', () => {
      const navStatusEl = document.querySelector('[data-navigation-status]');
      if (!navStatusEl) return;
      navStatusEl.setAttribute('data-navigation-status', 'not-active');
      // If you use Lenis you can 'start' Lenis here: Example Lenis.start();
    });
  });

  // ESC closes
  document.addEventListener('keydown', e => {
    if (e.keyCode === 27) {
      const navStatusEl = document.querySelector('[data-navigation-status]');
      if (!navStatusEl) return;
      if (navStatusEl.getAttribute('data-navigation-status') === 'active') {
        navStatusEl.setAttribute('data-navigation-status', 'not-active');
        // If you use Lenis you can 'start' Lenis here: Example Lenis.start();
      }
    }
  });
}

document.addEventListener('DOMContentLoaded', function() {
  initScalingHamburgerNavigation();
});

Attributes

NameTypeDefaultDescription
data-navigation-status"active" | "not-active""not-active"Placed on the <nav> element (or <body> for broader scope). Switching to "active" triggers the background pill expansion, content group scale-in, and toggle movement.
data-navigation-toggle="toggle"attributeAttach to the hamburger button element. Toggles data-navigation-status between active and not-active on click.
data-navigation-toggle="close"attributeAttach to any element that should always close the nav. The dark overlay (.navigation__dark-bg) has this by default.

Notes

  • No GSAP required — all animation is CSS-driven.
  • The dark background overlay (.navigation__dark-bg) doubles as a close trigger via data-navigation-toggle="close".
  • Pressing Escape closes the nav via the keydown listener.
  • Use aria-current="page" on the active link — the dot indicator and text dim are driven by this attribute.
  • To integrate Lenis, uncomment Lenis.stop() / Lenis.start() inside the toggle and close handlers.

Guide

Scale origin

The .hamburger-nav__group uses transform-origin: 100% 0 so the scale animation expands from the top-right corner — the same corner where the button is. The background pill (.hamburger-nav__bg) grows from width/height 3.5em to 100%/100% anchored to that same corner (top: 0; right: 0).

Toggle button movement

When the nav opens, the toggle button translates inward (translate(-1em, 1em)) so it sits visually inside the expanded panel rather than at the very edge. This keeps the X icon from being clipped by the panel border.

Dot indicator with :has()

The active page link shows a dot via aria-current. When any link is hovered, a :has() selector hides all dots, and the hovered link's dot reappears at reduced opacity. This creates a transfer effect without any JavaScript.

Marking the active link

Add aria-current="page" to the link for the current page. The CSS targets [aria-current] to dim the label text to 0.33 opacity and show the dot at full opacity.