Side Navigation with Wipe Effect
A right-anchored side navigation that opens with a layered panel wipe: three colored panels slide in from the right with staggered timing, followed by the menu links animating up from below and fade targets appearing last. Powered by GSAP and CustomEase.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/CustomEase.min.js"></script>Code
<div class="sidenav">
<header class="sidenav__header">
<button role="button" data-sidenav-toggle="" data-sidenav-button="" class="sidenav__button">
<div class="sidenav__button-text">
<p data-sidenav-label="" class="sidenav__button-label">Menu</p>
<p data-sidenav-label="" class="sidenav__button-label">Close</p>
</div>
<div data-sidenav-icon="" class="sidenav__button-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 16 16" fill="none" class="sidenav__button-icon-svg">
<path d="M7.33333 16L7.33333 -3.2055e-07L8.66667 -3.78832e-07L8.66667 16L7.33333 16Z" fill="currentColor"></path>
<path d="M16 8.66667L-2.62269e-07 8.66667L-3.78832e-07 7.33333L16 7.33333L16 8.66667Z" fill="currentColor"></path>
<path d="M6 7.33333L7.33333 7.33333L7.33333 6C7.33333 6.73637 6.73638 7.33333 6 7.33333Z" fill="currentColor"></path>
<path d="M10 7.33333L8.66667 7.33333L8.66667 6C8.66667 6.73638 9.26362 7.33333 10 7.33333Z" fill="currentColor"></path>
<path d="M6 8.66667L7.33333 8.66667L7.33333 10C7.33333 9.26362 6.73638 8.66667 6 8.66667Z" fill="currentColor"></path>
<path d="M10 8.66667L8.66667 8.66667L8.66667 10C8.66667 9.26362 9.26362 8.66667 10 8.66667Z" fill="currentColor"></path>
</svg>
</div>
</button>
</header>
<div data-sidenav-wrap="" data-nav-state="closed" class="sidenav__nav">
<div data-sidenav-overlay="" data-sidenav-toggle="" class="sidenav__overlay"></div>
<nav data-sidenav-menu="" class="sidenav__menu">
<div class="sidenav__menu-bg">
<div data-sidenav-panel="" class="sidenav__menu-bg-panel is--first"></div>
<div data-sidenav-panel="" class="sidenav__menu-bg-panel is--second"></div>
<div data-sidenav-panel="" class="sidenav__menu-bg-panel"></div>
</div>
<div class="sidenav__menu-inner">
<ul class="sidenav__menu-list">
<li class="sidenav__menu-list-item">
<a data-sidenav-link="" href="#" class="sidenav__menu-link">
<p class="sidenav__menu-link-heading">About us</p>
<p class="sidenav__menu-link-eyebrow">01</p>
</a>
</li>
<li class="sidenav__menu-list-item">
<a data-sidenav-link="" href="#" class="sidenav__menu-link">
<p class="sidenav__menu-link-heading">Our work</p>
<p class="sidenav__menu-link-eyebrow">02</p>
</a>
</li>
<li class="sidenav__menu-list-item">
<a data-sidenav-link="" href="#" class="sidenav__menu-link">
<p class="sidenav__menu-link-heading">Services</p>
<p class="sidenav__menu-link-eyebrow">03</p>
</a>
</li>
<li class="sidenav__menu-list-item">
<a data-sidenav-link="" href="#" class="sidenav__menu-link">
<p class="sidenav__menu-link-heading">Blog</p>
<p class="sidenav__menu-link-eyebrow">04</p>
</a>
</li>
<li class="sidenav__menu-list-item">
<a data-sidenav-link="" href="#" class="sidenav__menu-link">
<p class="sidenav__menu-link-heading">Contact us</p>
<p class="sidenav__menu-link-eyebrow">05</p>
</a>
</li>
</ul>
<div class="sidenav__menu-details">
<p data-sidenav-fade="" class="sidenav__button-label">Socials</p>
<div class="sidenav__menu-socials">
<a data-sidenav-fade="" href="#" class="sidenav__button-label">Instagram</a>
<a data-sidenav-fade="" href="#" class="sidenav__button-label">LinkedIn</a>
<a data-sidenav-fade="" href="#" class="sidenav__button-label">X/Twitter</a>
<a data-sidenav-fade="" href="#" class="sidenav__button-label">Awwwards</a>
</div>
</div>
</div>
</nav>
</div>
</div>.sidenav__header {
z-index: 10;
justify-content: flex-end;
align-items: center;
display: flex;
position: fixed;
top: 2em;
left: 2em;
right: 2em;
}
.sidenav__button {
z-index: 10;
grid-column-gap: .625em;
grid-row-gap: .625em;
background-color: #0000;
justify-content: flex-end;
align-items: center;
margin: -1em;
padding: 1em;
display: flex;
border: none;
}
.sidenav__button-text {
flex-flow: column;
justify-content: flex-start;
align-items: flex-end;
height: 1.5em;
display: flex;
overflow: hidden;
}
.sidenav__button-icon {
justify-content: center;
align-items: center;
width: 1em;
height: 1em;
transition: transform .4s cubic-bezier(.65, .05, 0, 1);
display: flex;
}
.sidenav__button-icon-svg {
width: 100%;
}
.sidenav__button-label {
color: #131313;
margin-top: 0;
margin-bottom: 0;
font-size: 1.125em;
line-height: 1.4;
}
.sidenav__nav {
z-index: 9;
width: 100%;
height: 100vh;
margin-left: auto;
margin-right: auto;
display: none;
position: fixed;
inset: 0%;
}
.sidenav__overlay {
z-index: 0;
cursor: pointer;
background-color: #13131366;
width: 100%;
height: 100%;
position: absolute;
inset: 0%;
}
.sidenav__menu {
grid-column-gap: 5em;
grid-row-gap: 5em;
flex-flow: column;
justify-content: space-between;
align-items: flex-start;
width: 35em;
height: 100%;
margin-left: auto;
padding-top: 6em;
padding-bottom: 2em;
position: relative;
overflow: auto;
}
.sidenav__menu-bg {
z-index: 0;
position: absolute;
inset: 0%;
}
.sidenav__menu-bg-panel {
z-index: 0;
background-color: #ebdcc5;
border-top-left-radius: 1.25em;
border-bottom-left-radius: 1.25em;
position: absolute;
inset: 0%;
}
.sidenav__menu-bg-panel.is--first {
background-color: #e04645;
}
.sidenav__menu-bg-panel.is--second {
background-color: #131313;
}
.sidenav__menu-inner {
z-index: 1;
grid-column-gap: 5em;
grid-row-gap: 5em;
flex-flow: column;
justify-content: space-between;
align-items: flex-start;
height: 100%;
display: flex;
position: relative;
overflow: auto;
}
.sidenav__menu-list {
flex-flow: column;
width: 100%;
margin-bottom: 0;
padding-left: 0;
list-style: none;
display: flex;
}
.sidenav__menu-list-item {
height: 6em;
margin-top: 0;
margin-bottom: 0;
position: relative;
overflow: hidden;
}
.sidenav__menu-link {
grid-column-gap: .75em;
grid-row-gap: .75em;
color: #131313;
width: 100%;
padding-top: .75em;
padding-bottom: .75em;
padding-left: 2em;
text-decoration: none;
display: flex;
}
.sidenav__menu-link-heading {
z-index: 1;
font-family: PP Neue Corp Tight, Arial, sans-serif;
font-size: 5.625em;
font-weight: 700;
line-height: .75;
transition: transform .55s cubic-bezier(.65, .05, 0, 1);
position: relative;
margin: 0px;
letter-spacing: -0.015em;
}
.sidenav__menu-link-eyebrow {
z-index: 1;
color: #e04645;
text-transform: uppercase;
font-family: RM Mono, Arial, sans-serif;
font-weight: 400;
position: relative;
}
.sidenav__menu-details {
grid-column-gap: 1.25em;
grid-row-gap: 1.25em;
flex-flow: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: 2em;
display: flex;
}
.sidenav__menu-socials {
grid-column-gap: 1.5em;
grid-row-gap: 1.5em;
flex-flow: row;
display: flex;
}
@media screen and (max-width: 767px) {
.sidenav__menu-socials {
grid-column-gap: 1em;
grid-row-gap: 1em;
}
.sidenav__menu-bg-panel {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.sidenav__menu {
width: 100%;
}
.sidenav__menu-list-item {
height: 4.5em;
}
.sidenav__menu-link-heading {
font-size: 4em;
}
}gsap.registerPlugin(CustomEase);
CustomEase.create("main", "0.65, 0.01, 0.05, 0.99");
gsap.defaults({
ease: "main",
duration: 0.7,
});
function initSideNavWipeEffect() {
const navWrap = document.querySelector("[data-sidenav-wrap]");
if (!navWrap) return;
const overlay = navWrap.querySelector("[data-sidenav-overlay]");
const menu = navWrap.querySelector("[data-sidenav-menu]");
const bgPanels = navWrap.querySelectorAll("[data-sidenav-panel]");
const menuToggles = document.querySelectorAll("[data-sidenav-toggle]");
const menuLinks = navWrap.querySelectorAll("[data-sidenav-link]");
const fadeTargets = navWrap.querySelectorAll("[data-sidenav-fade]");
const menuButton = document.querySelector("[data-sidenav-button]");
const menuButtonTexts = menuButton.querySelectorAll("[data-sidenav-label]");
const menuButtonIcon = menuButton.querySelector("[data-sidenav-icon]");
const tl = gsap.timeline();
const openNav = () => {
navWrap.setAttribute("data-nav-state", "open");
tl.clear()
.set(navWrap, { display: "block" })
.set(menu, { xPercent: 0 }, "<")
.fromTo(menuButtonTexts, { yPercent: 0 }, { yPercent: -100, stagger: 0.2 })
.fromTo(menuButtonIcon, { rotate: 0 }, { rotate: 315 }, "<")
.fromTo(overlay, { autoAlpha: 0 }, { autoAlpha: 1 }, "<")
.fromTo(bgPanels, { xPercent: 101 }, { xPercent: 0, stagger: 0.12, duration: 0.575 }, "<")
.fromTo(menuLinks, { yPercent: 140, rotate: 10 }, { yPercent: 0, rotate: 0, stagger: 0.05 }, "<+=0.35")
.fromTo(fadeTargets, { autoAlpha: 0, yPercent: 50 }, { autoAlpha: 1, yPercent: 0, stagger: 0.04 }, "<+=0.2");
};
const closeNav = () => {
navWrap.setAttribute("data-nav-state", "closed");
tl.clear()
.to(overlay, { autoAlpha: 0 })
.to(menu, { xPercent: 120 }, "<")
.to(menuButtonTexts, { yPercent: 0 }, "<")
.to(menuButtonIcon, { rotate: 0 }, "<")
.set(navWrap, { display: "none" });
};
menuToggles.forEach((toggle) => {
toggle.addEventListener("click", () => {
const state = navWrap.getAttribute("data-nav-state");
if (state === "open") {
closeNav();
} else {
openNav();
}
});
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && navWrap.getAttribute("data-nav-state") === "open") {
closeNav();
}
});
}
document.addEventListener("DOMContentLoaded", () => {
initSideNavWipeEffect();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| data-sidenav-wrap | attribute | — | Root wrapper for the entire nav overlay. Holds data-nav-state and is set to display:none/block by the GSAP timeline. |
| data-nav-state | "open" | "closed" | "closed" | Tracks whether the nav is open or closed. The script reads this on each toggle click to decide which animation to play. |
| data-sidenav-toggle | attribute | — | Attach to any element that should open or close the nav. Both the menu button and the background overlay carry this attribute. |
| data-sidenav-button | attribute | — | Identifies the main toggle button. Used to query the label texts and icon for the open/close label swap and icon rotation. |
| data-sidenav-label | attribute | — | Add to each label text element inside the button (e.g. "Menu" and "Close"). GSAP animates yPercent on these to slide between the two states. |
| data-sidenav-icon | attribute | — | The icon element inside the toggle button. GSAP rotates it from 0° to 315° on open and back to 0° on close. |
| data-sidenav-menu | attribute | — | The nav panel element. On close, GSAP slides it off-screen to the right via xPercent: 120. |
| data-sidenav-panel | attribute | — | Each colored background layer in the wipe effect. Three panels are staggered at 0.12s intervals sliding in from xPercent: 101. Panel colors are set via CSS combo-classes (.is--first, .is--second). |
| data-sidenav-link | attribute | — | Each navigation link. Animated from yPercent: 140 and rotate: 10 to their natural position with a 0.05s stagger. |
| data-sidenav-overlay | attribute | — | The dark semi-transparent page overlay. Fades in on open and out on close. Also carries data-sidenav-toggle to close the nav when clicked. |
| data-sidenav-fade | attribute | — | Secondary UI elements (labels, social links) that fade and slide up after the main wipe animation completes. |
Notes
- •Requires GSAP and CustomEase loaded via CDN before the script runs.
- •The overlay ([data-sidenav-overlay]) also carries [data-sidenav-toggle] so clicking outside the panel closes the nav.
- •Pressing Escape closes the nav when data-nav-state is open.
- •Panel colors are defined in CSS via .is--first and .is--second combo-classes — change them without touching JS.
- •On screens ≤767px the panel border-radius is removed and the menu expands to full width.
Guide
Panel wipe choreography
Three [data-sidenav-panel] elements are stacked absolutely on top of each other. They all start at xPercent: 101 (off-screen right) and animate in with a 0.12s stagger. The first panel (.is--first) is the accent color, the second (.is--second) is the dark color, and the last is the final menu background — so you see a quick color flash before the content appears.
Button label swap
The button contains two [data-sidenav-label] paragraphs stacked vertically ("Menu" on top, "Close" below) inside an overflow:hidden container. On open, both slide up by yPercent: -100 with a 0.2s stagger, revealing "Close". On close, they return to yPercent: 0.
Link reveal
Each [data-sidenav-link] starts at yPercent: 140 and rotate: 10, hidden by overflow:hidden on the parent list item. They animate in 0.35s after the panels start, with a 0.05s stagger per link.
CustomEase
The "main" CustomEase (0.65, 0.01, 0.05, 0.99) is registered globally and set as the default ease. This gives every tween a fast start that decelerates sharply into place.
CustomEase.create("main", "0.65, 0.01, 0.05, 0.99");
gsap.defaults({
ease: "main",
duration: 0.7,
});