Project Overview Modal — Click-to-Reveal Sliding Detail Overlay
Goal
Build a full-screen project index: a numbered list of projects sits at the bottom-left of a dark, non-scrolling viewport. Clicking any list row plays a paused GSAP timeline that slides a large white detail panel up from far off-screen while un-rotating it from a 20° tilt to flat, landing it pinned to the bottom-right. The panel's content (title, category, copy, link, image) is swapped in from a data array for whichever row you clicked. Clicking Close, or anywhere outside the panel, reverses the same timeline and the panel tilts and drops back off-screen. The star effect is that single tilt-corrected slide-up driven by one reversible timeline.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins, no ScrollTrigger, no Lenis, no SplitText.
import gsap from "gsap";
import { data } from "./data.js"; // project content lives in a sibling module
No framework. script.js is loaded as <script type="module" src="./script.js"> at the end of <body>, so it runs after the DOM is parsed — no DOMContentLoaded wrapper needed. The whole effect is one gsap.timeline({ paused: true }) with a single .to() tween, played/reversed on click. Project a Vite-style dev server that resolves the npm import.
Layout / HTML
Four top-level blocks in <body>: a fixed .nav, a fixed .footer, the hidden .overlay panel, and the .container holding the list. Class/ID names are load-bearing — the JS/CSS query them.
<body>
<div class="nav">
<p>Nanotech</p> <!-- fictional demo brand, top-left -->
<p>Showreel</p> <!-- top-right -->
</div>
<div class="footer">
<p>Nanotech 2023 ©</p> <!-- bottom-right -->
</div>
<!-- The white detail panel, parked far off-screen and tilted -->
<div class="overlay">
<div class="overlay-header">
<div class="col">
<h1 id="item-name">Item 1</h1>
<p id="item-category">Item Category</p>
</div>
<div class="col">
<p id="close-btn">Close</p>
</div>
</div>
<div class="item-details">
<p><a id="item-link" href="#"><i class="icon-arrow-up-right"></i> View Site</a></p>
<p id="item-copy">Lorem ipsum dolor sit amet…</p>
</div>
<div class="img-container">
<img id="item-img" src="(img-1)" alt="" />
</div>
</div>
<!-- The project index -->
<div class="container">
<div class="items">
<div class="item">
<div class="item-index">01</div>
<div class="item-name">Advanced Nanotech Fabrics</div>
<div class="item-year">2022</div>
</div>
<!-- …9 more .item rows… -->
</div>
</div>
<script type="module" src="./script.js"></script>
</body>
The 10 list rows (index / name / year — three flex columns per row):
01 Advanced Nanotech Fabrics 2022
02 Natural Bio-luminescent Accents 2021
03 Integrated Electronic Wearables 2023
04 Dynamic Morphing Clothing 2021
05 3D Holographic Prints 2017
06 Atmospheric Aero-adaptable Garments 2018
07 Responsive Skinsync Attire 2019
08 Sustainable Zero-waste Weavings 2023
09 Lab-crafted Synthetic Leather 2022
10 Neuro-responsive Fashion Gear 2021
The link's leading <i> is an arrow-up-right glyph (original uses a Phosphor icon font <i class="ph-bold ph-arrow-up-right"> loaded via CDN <script>; any icon font or inline SVG arrow works, or omit it).
data.js — the swap-in content (one object per list row, same order)
Export const data = [ … ]; export { data };. Each object has itemName, itemCategory, itemLink (all "#"), itemCopy (a ~50-word marketing paragraph), and itemImg (path to one of the 5 images). The overlay itemName is a shortened form of the list-row name (e.g. row "Advanced Nanotech Fabrics" → overlay "Nanotech Fabrics"). Categories cycle among a few labels like "Eco-Futurism Line", "Neural Nuance", "Galactic Glam", "Digital Elegance". Images cycle 1→5 then repeat: items 1 & 6 use img-1, items 2 & 7 use img-2, 3 & 8 → img-3, 4 & 9 → img-4, 5 & 10 → img-5.
Styling
Global reset * { margin:0; padding:0; box-sizing:border-box }. Font: a neutral sans-serif (original references a proprietary face "Aeonik"; any clean grotesque like Inter / system-ui matches).
Palette (exact hex):
#0f0f0f— page background (near-black).#fff— page text, and the overlay panel background.#000— text color inside the white overlay (and link color).
Page shell — the page never scrolls:
html, body { width:100vw; height:100vh; overflow:hidden; background:#0f0f0f; color:#fff; }
h1 { font-weight:500; margin-bottom:0.5em; }
a { text-decoration:none; color:#000; }
img { width:100%; height:100%; object-fit:cover; }
Nav / footer (fixed chrome):
.nav { position:fixed; width:100%; padding:2em; display:flex; justify-content:space-between; }
.footer { position:fixed; bottom:0; right:0; padding:2em; }
Container + list (bottom-left):
.container { width:100%; height:100%; display:flex; justify-content:flex-start; align-items:flex-end; padding:2em; }
.items { position:absolute; width:50%; display:flex; flex-direction:column; } /* left half */
.item { display:flex; padding:0.25em 0.5em; cursor:pointer; }
.item-index { flex:1; } /* narrow */
.item-name { flex:4; } /* wide */
.item-year { flex:1; } /* narrow */
The overlay panel — this CSS defines the animation's START state. It is parked 1200px below the viewport and rotated 20deg, tilting up from its bottom-center pivot:
.overlay {
position:absolute; bottom:-1200px; right:0; /* far below the screen */
width:70%; height:700px; padding:2em;
background:#fff; color:#000;
overflow-x:hidden; overflow-y:scroll; /* the panel scrolls internally… */
z-index:2;
will-change:bottom;
transform:translateZ(0) rotate(20deg); /* …tilted 20° at rest */
transform-origin:bottom center;
}
.overlay::-webkit-scrollbar { display:none; } /* hide its scrollbar */
Overlay internals:
.overlay-header { width:100%; display:flex; justify-content:space-between; align-items:flex-end; padding:2em 0; }
.col:nth-child(1) { flex:3; } /* title + category */
.col:nth-child(2) { flex:2; } /* Close */
#close-btn { cursor:pointer; opacity:0.4; }
.item-details { width:100%; display:flex; justify-content:space-between; padding:1em 0 4em 0; }
.item-details p:nth-child(1) { flex:3; } /* the View-Site link */
.item-details p:nth-child(2) { flex:2; } /* the copy paragraph */
.img-container { width:100%; padding-bottom:2em; } /* the detail image fills the panel width */
GSAP effect (be exhaustive)
There is exactly one paused timeline holding one tween, plus three click handlers. All positional/rotational start values live in CSS (above); the timeline animates them to the resting/on-screen state.
The timeline (built once, on load)
const overlay = document.querySelector(".overlay");
const closeBtn = document.querySelector("#close-btn");
const tl = gsap.timeline({ paused: true, overwrite: "auto" });
tl.to(overlay, {
duration: 0.5,
bottom: "0px", // slides UP: bottom -1200px → 0px
rotation: 0, // un-tilts: 20deg → 0deg
transformOrigin: "bottom center",
ease: "power2.out",
});
paused: true— nothing plays until a click.overwrite: "auto"— a rapid re-click won't stack conflicting tweens.- The tween animates the panel from its CSS start state (
bottom:-1200px,rotate(20deg)) tobottom:0px,rotation:0in 0.5 s withpower2.out(fast start, gentle settle). Because it pivots aboutbottom center, the panel appears to swing upright as it rises into the bottom-right corner. - No stagger, no delay, no labels — a single tween is the whole timeline.
Open — click a list row → tl.play() + swap content
const items = document.querySelectorAll(".item");
items.forEach((item, index) => {
item.addEventListener("click", () => {
tl.play(); // play forward from current position
updateOverlay(data[index]); // fill panel with THIS row's data
});
});
function updateOverlay(dataItem) {
const itemName = document.querySelector("#item-category").previousElementSibling; // the <h1>
const itemCategory = document.querySelector("#item-category");
const itemLink = document.querySelector("#item-link");
const itemCopy = document.querySelector("#item-copy");
const itemImg = document.querySelector("#item-img");
itemName.textContent = dataItem.itemName;
itemCategory.textContent = dataItem.itemCategory;
itemLink.href = dataItem.itemLink;
itemCopy.textContent = dataItem.itemCopy;
itemImg.src = dataItem.itemImg;
}
tl.play() always drives the same timeline forward, so clicking a different row while the panel is already open just re-fills the content in place (the panel is already at bottom:0, so the tween has nothing left to travel).
Close — button, and click-outside → tl.reverse()
closeBtn.addEventListener("click", () => { tl.reverse(); });
document.addEventListener("click", (e) => {
if (!overlay.contains(e.target) && !isItem(e.target)) {
tl.reverse(); // clicking empty space closes the panel
}
});
function isItem(target) { return target.closest(".item"); }
tl.reverse() plays the exact same tween backward: the panel re-tilts to 20° and drops back to bottom:-1200px over 0.5 s (the power2.out ease reads as power2.in in reverse). The document-level guard reverses on any click that is neither inside the overlay nor on a list item, so clicking a row never immediately re-closes it.
Net choreography
- Load: panel invisible, parked 1200px below and tilted 20°.
- Click row *n*: content swaps to
data[n]; panel slides up + straightens into the bottom-right corner in 0.5 s (power2.out). - Click Close or any empty area: panel tilts back + slides down off-screen in 0.5 s (reversed tween).
No SplitText, CustomEase, lerp/rAF loop, ScrollTrigger, Three.js, or canvas — just this one reversible power2.out slide-and-rotate.
Assets / images
5 editorial fashion photographs, full-bleed (shown object-fit:cover, filling the panel's full width — roughly portrait/3:4). Mood: futuristic, high-fashion, avant-garde — models in sculptural techwear / iridescent fabrics / metallic and bio-luminescent styling against clean studio or moody sets. They are the detail image shown inside the white overlay, one per project, cycling img-1…img-5 and repeating for rows 6–10. No brands or logos.
Behavior notes
- No scroll on the page —
html, body { overflow:hidden }; the only scrollable region is inside the open overlay (its scrollbar hidden). The effect is entirely click-driven, not scroll-driven. - Fully reversible off one timeline: the same 0.5 s tween opens and closes; state is preserved so mid-flight clicks just reverse direction (
overwrite:"auto"guards against tween pile-up). - Responsive (
max-width: 900px):.items { width:100%; bottom:8em; }(list spans full width, lifted off the bottom);.overlay { width:100%; height:100vh; }(panel becomes a full-screen sheet). The GSAP tween is unchanged. - No reduced-motion branch in the original; if adding one, swap the slide for an instant show/hide.