Burger Menu Button
A GSAP-animated burger-to-close button. The three lines animate in a multi-step sequence: the middle line shrinks, the outer lines exit, instantly reposition at crossed angles, then animate into an X. Closing reverses all lines back to their original state in one smooth tween.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/CustomEase.min.js"></script>Code
<button data-menu-button="burger" class="menu-button">
<div class="menu-button-line"></div>
<div class="menu-button-line"></div>
<div class="menu-button-line"></div>
</button>.menu-button {
grid-column-gap: .1875em;
grid-row-gap: .1875em;
flex-flow: column;
padding: 1em;
font-size: 1em;
display: flex;
background: transparent;
-webkit-appearance: none;
border: none;
}
.menu-button-line {
background-color: #e7dddb;
width: 2em;
height: .1875em;
}gsap.registerPlugin(CustomEase)
CustomEase.create("button-ease", "0.5, 0.05, 0.05, 0.99")
function initMenuButton() {
// Select elements
const menuButton = document.querySelector("[data-menu-button]");
const lines = document.querySelectorAll(".menu-button-line");
const [line1, line2, line3] = lines;
// Define one global timeline
let menuButtonTl = gsap.timeline({
defaults: {
overwrite: "auto",
ease: "button-ease",
duration: 0.3
}
})
const menuOpen = () => {
menuButtonTl.clear() // Stop any previous tweens, if any
.to(line2, { scaleX: 0, opacity: 0 }) // Step 1: Hide middle line
.to(line1, { x: "-1.3em", opacity: 0 }, "<") // Step 1: Move top line
.to(line3, { x: "1.3em", opacity: 0 }, "<") // Step 1: Move bottom line
.to([line1, line3], { opacity: 0, duration: 0.1 }, "<+=0.2") // Step 2: Quickly fade top and bottom lines
.set(line1, { rotate: -135, y: "-1.3em", scaleX: 0.9 }) // Step 3: Instantly rotate and scale top line
.set(line3, { rotate: 135, y: "-1.4em", scaleX: 0.9 }, "<") // Step 3: Instantly rotate and scale bottom line
.to(line1, { opacity: 1, x: "0em", y: "0.5em" }) // Step 4: Move top line to final position
.to(line3, { opacity: 1, x: "0em", y: "-0.25em" }, "<+=0.1"); // Step 4: Move bottom line to final position
}
const menuClose = () => {
menuButtonTl.clear() // Stop any previous tweens, if any
.to([line1, line2, line3], { // Move all lines back in a single animation
scaleX: 1,
rotate: 0,
x: "0em",
y: "0em",
opacity: 1,
duration: 0.45,
overwrite: "auto",
})
}
// Toggle Animation
menuButton.addEventListener("click", () => {
const currentState = menuButton.getAttribute("data-menu-button");
if (currentState === "burger") {
menuOpen()
menuButton.setAttribute("data-menu-button", "close");
} else {
menuClose()
menuButton.setAttribute("data-menu-button", "burger");
}
});
}
// Initialize Burger Menu Button
document.addEventListener('DOMContentLoaded', () => {
initMenuButton();
});Guide
Open Sequence
The open animation runs in 4 steps: (1) middle line shrinks and outer lines exit in opposite directions, (2) outer lines quickly fade out, (3) they instantly reposition at ±135° rotations and negative Y offsets via gsap.set, (4) they animate back to center, forming an X.
Close Sequence
The close animation is a single tween that resets all three lines — scale, rotation, x, y, and opacity — back to their original values in one smooth motion.
State Tracking
The data-menu-button attribute toggles between "burger" and "close" on each click, driving both the animation direction and providing a CSS hook if you need to style other elements based on menu state.
Timeline Management
menuButtonTl.clear() kills all pending tweens before starting a new sequence, so rapid clicking never causes the animations to stack or fight each other.
Custom Ease
The "button-ease" CustomEase curve (0.5, 0.05, 0.05, 0.99) gives a fast start that decelerates sharply at the end, producing a snappy, mechanical feel. Adjust the four cubic-bezier values to change the character of the animation.