Sidebar Slide-Out Menu — Staggered Cards Sliding In From The Right + Background Blur
Goal
Build a fullscreen hero page with a small "Get the Overlay" toggle pinned in the bottom-right corner. Clicking it slides a fixed column of three white cards in from the right edge of the screen with a fast GSAP 0.075s stagger on a power4.out ease, while the whole page content blurs to 15px and the toggle button itself slides off-screen to the right. Clicking any card reverses everything — the cards slide back out past the right edge, the toggle slides back into its corner, and the page un-blurs. On top of that, each card has pure-CSS hover behavior: hovering a card swaps it to a deep-blue fill and reveals its body copy, and the first card additionally pops in a large close (×) icon. It is entirely click- and hover-driven — no scroll, no autoplay.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins, no ScrollTrigger, no smooth-scroll library. Import as:
import gsap from "gsap";
Icons come from the Phosphor Icons web font, loaded via a <script> tag in <head>:
<script src="https://unpkg.com/@phosphor-icons/web"></script>
Used icon classes: ph ph-arrow-right (the arrow in the toggle) and ph-thin ph-x (the close × on card 1).
Layout / HTML
.container (fullscreen, position:relative — holds the site + toggle; this is what blurs)
.website-content
nav (fixed top bar, flex, 3 equal columns)
.logo "Motionprompts"
.cta "Subscribe" (center column)
.links (right column, flex-end)
a "About" a "Infos" a "Contact"
.hero (left-aligned headline block, vertically centered)
h1 "Elite web designs"
p (a paragraph of body copy)
.overlay-toggle (bottom-right white pill — the OPEN click target)
p "Get the Overlay"
p > i.ph.ph-arrow-right
.sidebar (fixed full-height column on the right; clips the cards)
.card (card 1)
.card-title
div "Showreel"
.close-btn > i.ph-thin.ph-x (large × — only on card 1)
.card-copy > p (body copy, hidden until hover)
.card (card 2)
.card-title "Community"
.card-copy > p
.card (card 3)
.card-title "Catalog"
.card-copy > p
Note the .sidebar is a sibling of .container, not inside it — so when .container blurs, the sidebar cards stay sharp. Use neutral, fictional labels (Motionprompts / Subscribe / About / Infos / Contact / Showreel / Community / Catalog) — no real brand names. Card body copies are short lorem-ipsum sentences.
Styling
Font: "Neue Montreal" (a clean neutral grotesque sans-serif). If unavailable, fall back to a similar sans (system-ui, Helvetica). html, body { width:100%; height:100%; background:#000; color:#fff; font-family:"Neue Montreal"; }.
Global reset: * { margin:0; padding:0; box-sizing:border-box; }.
Color tokens:
- Page background:
#000(black) - Text on hero:
#fff; hero paragraph muted grey#a0a0a0 - Toggle pill + cards default fill:
#fffwith#000text - Card hover fill:
#020B44(deep navy blue) with#ffftext
Structural / load-bearing CSS (these dimensions and the right offsets are what the GSAP tweens animate to/from — keep them exact):
.container:position: relative; width: 100vw; height: 100vh;fullscreen hero background imageno-repeat 50% 50%,background-size: cover.nav:position: fixed; width: 100%; display: flex; padding: 1.5em;withnav > div { flex: 1; }..cta { text-align: center; }..links { display: flex; justify-content: flex-end; gap: 2em; }.a { text-decoration: none; color: #fff; }..hero:position: absolute; top: 50%; transform: translateY(-50%); padding: 0 1.5em; width: 35%;..hero h1 { font-size: 40px; font-weight: 400; margin-bottom: 10px; }..hero p { color: #a0a0a0; line-height: 1.5; }..overlay-toggle:position: absolute; right: 0; bottom: 0; width: 250px; height: 150px; margin: 0.75em; padding: 1em; border-radius: 0.5em; background: #fff; color: #000; display: flex; justify-content: space-between; cursor: pointer;(label top-left, arrow icon top-right). Itsrightis the animated property (starts at the CSS0, effectivelyright: 0.75emvisually via the margin)..sidebar:position: fixed; top: 0; right: 0; width: 30vw; height: 100vh; padding: 0.75em; display: flex; flex-direction: column; gap: 0.75em; overflow: hidden; pointer-events: none;.overflow: hiddenclips the cards while they sit off-screen atright: -110%;pointer-events: noneis the closed state (JS toggles it toallwhen open so the cards become clickable)..card:position: relative; right: -110%;(this off-screen offset is the closed state the GSAP tween animates from)padding: 1em; flex: 1; background: #fff; color: #000; border-radius: 0.5em; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; cursor: pointer;. Three equal-height cards stacked vertically..card-title:font-size: 50px; letter-spacing: -0.035em;..card:nth-child(1) .card-title { display: flex; justify-content: space-between; }(title text on the left, close-btn on the right — only card 1)..close-btn:position: relative; font-size: 60px; transform: scale(0); transition: transform 0.3s; transform-origin: center;— hidden by default, scaled up on card-1 hover..card-copy:position: relative; opacity: 0; transform: translateY(20px); font-size: 15px; transition: transform 0.3s, opacity 0.3s;— hidden by default, revealed on hover.
Pure-CSS hover behavior (NOT GSAP):
.card:hover { background: #020B44; color: #fff; }
.card:hover .card-copy { opacity: 1; transform: translateY(0px); } /* copy slides up + fades in over 0.3s */
.card:nth-child(1):hover .close-btn { transform: scale(1); } /* × pops from scale 0→1 over 0.3s */
GSAP effect (exhaustive)
No plugins, no timeline object — the effect is built from standalone gsap.to() tweens fired inside two functions (animateCardsIn on open, animateCardsOut on close). Grab refs once on DOMContentLoaded:
const container = document.querySelector(".container");
const sidebar = document.querySelector(".sidebar");
const cards = gsap.utils.toArray(".card"); // the 3 cards, in DOM order
const overlayToggle = document.querySelector(".overlay-toggle");
Open — animateCardsIn()
Fires three parallel standalone tweens, all starting on the same tick (see note on "<" below):
- Toggle slides off-screen right:
``js gsap.to(overlayToggle, { right: "-500px", duration: 1, ease: "power4.out" }); ` Animates the pill's right from its resting 0 to -500px, sliding it out past the right edge. duration: 1, ease: "power4.out"`.
- Cards slide in, staggered:
``js gsap.to(cards, { right: "0%", stagger: 0.075, duration: 1, ease: "power4.out" }, "<"); ` Animates each card's right from the CSS -110% (fully off-screen to the right, clipped by the sidebar's overflow: hidden) to 0% (flush in place). stagger: 0.075 fires them 0.075s apart in DOM order — card 1 first (Showreel), then card 2 (Community), then card 3 (Catalog). duration: 1 each, ease: "power4.out"` (fast launch, long smooth settle).
- Page content blurs:
``js gsap.to(container, { filter: "blur(15px)", duration: 1, immediateRender: false }, "<"); ` Animates the .container's CSS filter from blur(0px) to blur(15px) over duration: 1. No ease specified → GSAP default power1.out. immediateRender: false prevents GSAP from snapping the blur to its end value before the tween runs. Because the sidebar is a sibling (outside .container`), the incoming cards remain crisp while the site behind them goes soft.
Close — animateCardsOut()
The mirror image, again three parallel standalone tweens:
- Toggle slides back into its corner:
``js gsap.to(overlayToggle, { right: "0px", duration: 1, ease: "power4.out" }); ` right: -500px → 0px, duration: 1, ease: "power4.out"`.
- Cards slide back out, staggered:
``js gsap.to(cards, { right: "-110%", stagger: 0.075, duration: 1, ease: "power4.out" }); ` right: 0% → -110% (back off-screen), same stagger: 0.075 in DOM order, duration: 1, ease: "power4.out"`.
- Page un-blurs:
``js gsap.to(container, { filter: "blur(0px)", duration: 1, immediateRender: false }, "<"); ` blur(15px) → blur(0px), duration: 1, default power1.out ease, immediateRender: false`.
Note on the "<" third argument: in the source it is passed to some of these standalone gsap.to() calls, but the position parameter ("<") is only meaningful on timeline methods — on the global gsap.to() it is simply ignored. The practical result is that all three tweens in each function start together on the same tick. (You may include it verbatim for fidelity, or omit it — behavior is identical.)
Wiring
overlayToggle.addEventListener("click", () => {
sidebar.style.pointerEvents = "all"; // enable card clicks while open
animateCardsIn();
});
cards.forEach((card) => {
card.addEventListener("click", () => {
sidebar.style.pointerEvents = "none"; // disable card clicks while closed
animateCardsOut();
});
});
- Open: clicking the
.overlay-togglesets the sidebar topointer-events: all(so the cards become clickable) and runsanimateCardsIn(). - Close: clicking any card sets the sidebar back to
pointer-events: noneand runsanimateCardsOut(). (Every card closes the menu, including card 1 — the × icon is decorative CSS, not a separate handler.) - There is no re-entrancy guard; GSAP overwrites in-flight tweens naturally if the user clicks rapidly.
Wrap everything in document.addEventListener("DOMContentLoaded", () => { ... }).
Assets / images
- 1 hero background image — role: *fullscreen background behind the whole site, sits inside
.containerand is what blurs*. A moody, dark, photographic image (low-key, cinematic). Displayedbackground-size: coverat50% 50%. Landscape ~16:9 (e.g. 1920×1080 or larger). Because the UI text is white and the card hover fill is a deep navy, a darker image reads best.
Behavior notes
- Entirely click + hover driven — no scroll, no ScrollTrigger, no loops, no autoplay.
- The card enter/exit + toggle slide + page blur are GSAP; the card color swap, body-copy reveal, and card-1 × pop are pure CSS
:hovertransitions (all0.3s). - Closed state is expressed in CSS (
.card { right: -110% },.sidebar { pointer-events: none }); JS only ever tweens toward/away from it and togglespointer-events. - Responsive (
max-width: 900px):.hero { top: 25%; width: 100%; }and.sidebar { width: 100vw; }(the sidebar becomes a full-width overlay; cards still slide in from the right with the same stagger).