All components

Project Overview Modal

GSAP animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

A project index where clicking any list item plays a paused GSAP timeline that slides a white detail overlay up from off-screen (bottom -1200px to 0) while un-rotating it from a 20deg tilt to flat with a power2.out ease; clicking Close or anywhere outside reverses the same timeline, and the overlay content (name, category, copy, link, image) is swapped in from a data array on open.

How it's built

Categoryinteractive
Techgsap
Complexitypage
Performance costlight
Mobile-safeyes

modal overlay gsap timeline click reveal portfolio editorial rotate

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

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 &copy;</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)) to bottom:0px, rotation:0 in 0.5 s with power2.out (fast start, gentle settle). Because it pivots about bottom 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
  1. Load: panel invisible, parked 1200px below and tilted 20°.
  2. Click row *n*: content swaps to data[n]; panel slides up + straightens into the bottom-right corner in 0.5 s (power2.out).
  3. 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-1img-5 and repeating for rows 6–10. No brands or logos.

Behavior notes

  • No scroll on the pagehtml, 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.