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.
Code
<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>.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);
}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
| Name | Type | Default | Description |
|---|---|---|---|
| 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" | attribute | — | Attach to the hamburger button element. Toggles data-navigation-status between active and not-active on click. |
| data-navigation-toggle="close" | attribute | — | Attach 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.