All components

Modular Carousel

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

Open live demo ↗ Raw prompt (.md)

What it does

A click-driven modular image carousel: each click advances a horizontal strip of slide titles via a 2s gsap.to power4.out tween, while two split image panels (top/bottom, shaped with CSS clip-path polygons) are freshly injected and revealed by animating their clip-path and scale from collapsed to full with a 0.15s stagger. Hovering the image widens the top/bottom clip-path windows; excess stacked images are trimmed on tween complete.

How it's built

Categoryslider
Techgsap
Complexitypage
Performance costlight
Mobile-safeyes

carousel clip-path gsap image-reveal split-panels click-driven architecture editorial dark

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

Modular Carousel — click-advanced split-panel clip-path image slider

Goal

Build a full-viewport, click-driven image carousel. The whole page is the button: every click anywhere advances the slider. Two things happen per click, in parallel: (1) a horizontal strip of big centered slide titles slides one slot to the left via a slow 2s gsap.to power4.out tween, promoting a new title to the highlighted center position; and (2) in a small centered stage made of two offset split panels (a top band and a bottom band cut with CSS clip-path polygons), a fresh pair of <img> elements is injected and revealed — their own animated clip-path wipes open from the right edge while they simultaneously scale from 2 → 1, with a 0.15s stagger between the top and bottom panel. The star effect is this layered clip-path reveal: each new image is stacked on top of the previous ones and sweeps in; old images are trimmed away after the tween completes. Hovering the image stage widens the two clip-path windows via a pure-CSS transition.

Tech

Vanilla HTML / CSS / JS with an ES module entry (<script type="module" src="./script.js">), bundled by Vite. gsap (npm) is the only dependency — import gsap from "gsap". No GSAP plugins (no ScrollTrigger, SplitText, CustomEase), no Lenis, no Three.js, no canvas. GSAP is used only for two gsap.to tweens (the title strip transform and the image clip-path/scale reveal). Everything else — the split-panel windows and the hover widening — is plain CSS clip-path + CSS transition. Must run in a fresh Vite project with only gsap installed.

Layout / HTML

Static chrome (fixed nav + footer) plus the slider. The 9 title elements are hard-coded in the HTML; the image <img> elements are not in the HTML — JS injects them at runtime.

<nav>
  <a href="#">Motionprompts</a>
  <p>Unlock Source Code with PRO</p>
</nav>

<footer>
  <div class="links">
    <a href="#">Subscribe</a>
    <a href="#">Instagram</a>
    <a href="#">Twitter</a>
  </div>
  <p>Link in description</p>
</footer>

<div class="slider">
  <div class="slide-titles">
    <div class="title"><h1>Neo Forge Towers</h1></div>
    <div class="title"><h1>Arcadian Complex</h1></div>
    <div class="title"><h1>Shadowline Spire</h1></div>
    <div class="title"><h1>Echo Nexus Habitat</h1></div>
    <div class="title"><h1>Cascade Enclave</h1></div>
    <div class="title"><h1>Prism Sector</h1></div>
    <div class="title"><h1>Iron Eden Colony</h1></div>
    <div class="title"><h1>Neo Forge Towers</h1></div>
    <div class="title"><h1>Arcadian Complex</h1></div>
  </div>
  <div class="slide-images">
    <div class="img-top"></div>
    <div class="img-bottom"></div>
  </div>
</div>
  • Exactly 9 .title blocks. There are only 7 unique titles; the last two

(Neo Forge Towers, Arcadian Complex) intentionally repeat the first two so the loop never reveals an empty edge. Titles are fictional futuristic-architecture names — keep them or swap for any neutral set, but keep 9 of them with the first two repeated at the end.

  • .slide-images holds two empty panel containers, .img-top and .img-bottom. JS appends the

injected <img> elements into these.

Styling

Global reset: * { margin: 0; padding: 0; box-sizing: border-box; }

  • Page: `html, body { width: 100vw; height: 100vh; overflow: hidden; background: #0f0f0f;

font-family: "Circular Std"; }` — dark near-black stage. If Circular Std is unavailable, use any clean geometric sans (e.g. a neutral grotesque) as fallback.

  • img (global) — `position: absolute; top: 0; left: 0; width: 100%; height: 100%;

object-fit: cover; Every injected image fills its panel box and is cropped by cover`.

  • nav, footer — `position: fixed; width: 100%; padding: 2em; display: flex;

justify-content: space-between; align-items: center; z-index: 2; mix-blend-mode: difference; nav { top: 0; } footer { bottom: 0; }. The mix-blend-mode: difference` makes the white labels invert against whatever passes beneath them.

  • a, pcolor: #fff; font-size: 14px; text-decoration: none;. `.links { display: flex;

gap: 2em; }`

  • .sliderwidth: 100vw; height: 100vh;
  • .slide-titles — `position: absolute; top: 0; left: 0; width: 300vw; height: 100vh;

display: flex; pointer-events: none; z-index: 2;` A 300vw-wide flex row of the 9 titles.

  • .title — `flex: 1; width: 100%; height: 100%; display: flex; justify-content: center;

align-items: center; Each title is 300vw / 9 = 33.33vw` wide, so three titles span the viewport and exactly one sits centered on screen at any resting position.

  • .title h1 — `text-align: center; font-size: 28px; font-weight: 400;

color: rgba(255, 255, 255, 0.2); transition: color 0.25s ease, opacity 0.25s ease;` Dim by default (20% white).

  • .active h1color: #fff; The centered/active title brightens to solid white; the

0.25s color transition makes the highlight cross-fade as slides change.

  • .slide-images — `width: 550px; height: 500px; position: absolute; top: 50%; left: 50%;

transform: translate(-50%, -50%); pointer-events: all; opacity: 0.5;` A 550×500px near-square stage centered in the viewport, held at 50% opacity so the images read as dim/atmospheric behind the titles.

  • .img-top — `position: absolute; width: 100%; height: 100%;

clip-path: polygon(85% 0%, 0% 0%, 0% 50%, 85% 50%); transition: clip-path 1s cubic-bezier(0.075, 0.82, 0.165, 1);` A rectangular window covering the top-left band (x: 0→85%, y: 0→50%), shifted left.

  • .img-bottom — `position: absolute; width: 100%; height: 100%;

clip-path: polygon(100% 50%, 15% 50%, 15% 100%, 100% 100%); transition: clip-path 1s cubic-bezier(0.075, 0.82, 0.165, 1);` A rectangular window covering the bottom-right band (x: 15→100%, y: 50→100%), shifted right. Together the two offset bands make the signature staggered "modular" split — the top half pushed left, the bottom half pushed right.

  • Hover (cubic-bezier(0.075, 0.82, 0.165, 1) = ease-out-circ over 1s):

.slide-images:hover .img-top { clip-path: polygon(90% 0%, 10% 0%, 10% 50%, 90% 50%); } and .slide-images:hover .img-bottom { clip-path: polygon(90% 50%, 10% 50%, 10% 100%, 90% 100%); } — both bands slide toward center and become symmetric (x: 10→90%), so the split closes into a centered, aligned pair while hovered.

The effect — exhaustive GSAP + interaction spec

State (module scope)
let currentIndex = 1;   // which title index is centered/active (0-based into the 9 .title nodes)
let totalSlides  = 7;   // number of unique slides in the loop
Active-title highlighting — updateActiveSlide()

Loop over all .title nodes; add class active to the one whose index equals currentIndex, remove it from all others. Only the active title's h1 is solid white (via .active h1), the 0.25s CSS transition cross-fades the highlight.

Click handler — handleSlider() (bound to document click)

A click anywhere on the page triggers one advance:

  1. Advance the index with a 7-state wrap: `if (currentIndex < totalSlides) currentIndex++;

else currentIndex = 1;. So the index cycles 1 → 2 → 3 → 4 → 5 → 6 → 7 → 1 → …` (it never sits at 0).

  1. Tween the whole title strip left by exactly one title-width:
gsap.to(".slide-titles", {
  x: `-${(currentIndex - 1) * 11.1111}%`,   // percentage of the element's OWN width (300vw)
  duration: 2,
  ease: "power4.out",
  onStart: () => {
    setTimeout(() => { updateActiveSlide(); }, 100);   // promote new active title 100ms in
    updateImages(currentIndex + 1);                    // reveal the NEXT image (see offset note)
  },
});
  • x is a percentage string, so GSAP writes it as a translateX relative to the strip's own

300vw width. 11.1111% of 300vw = 33.33vw = exactly one title slot. At currentIndex=1, x = 0; at currentIndex=2, x = -11.1111%; … at currentIndex=7, x = -66.66%. Each step glides the row one slot left over 2s with power4.out (fast start, long soft settle).

  • The active title updates 100ms after the tween starts (via setTimeout), not at the end —

the highlight jumps to the incoming title early while the strip is still gliding.

Image reveal — updateImages(imageNumber)

Called with an image number; injects and animates a fresh pair of stacked images:

const imgSrc = `/c/modular-slider/img${imageNumber}.jpg`;
const imgTop = document.createElement("img");
const imgBottom = document.createElement("img");
imgTop.src = imgSrc;
imgBottom.src = imgSrc;

// initial (collapsed) state — a zero-width sliver pinned at the RIGHT edge, zoomed 2×:
imgTop.style.clipPath    = "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)";
imgBottom.style.clipPath = "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)";
imgTop.style.transform    = "scale(2)";
imgBottom.style.transform = "scale(2)";

document.querySelector(".img-top").appendChild(imgTop);
document.querySelector(".img-bottom").appendChild(imgBottom);

gsap.to([imgTop, imgBottom], {
  clipPath: "polygon(100% 0%, 0% 0%, 0% 100%, 100% 100%)",  // full rectangle
  transform: "scale(1)",
  duration: 2,
  ease: "power4.out",
  stagger: 0.15,          // top panel reveals, bottom panel 0.15s behind
  onComplete: trimExcessImages,
});

Reveal mechanics to reproduce exactly:

  • Both panels get the SAME image (imgTop and imgBottom share imgSrc); the container

clip-paths (.img-top / .img-bottom) carve them into the offset top/bottom bands, so the two copies together look like one image split across the two windows.

  • The animated clip-path goes from a degenerate polygon collapsed at the right edge

(all four points at x = 100%) to the full rectangle (polygon(100% 0%, 0% 0%, 0% 100%, 100% 100%)). The two left corners travel from x = 100% to x = 0%, so the image wipes open from the right edge leftward.

  • Simultaneously scale(2) → scale(1) — a zoom-out that resolves as the wipe completes.
  • power4.out, duration: 2, with a 0.15s stagger so the bottom band trails the top band.
  • Each call appends new <img> nodes on top of the previous ones (absolute-positioned stack),

so the freshly revealed image slides in over the still-visible previous one.

Trimming the stack — trimExcessImages() (the reveal tween's onComplete)

For each of .img-top and .img-bottom: collect its child <img>s and, if there are more than 5, remove the oldest ones (images.slice(0, images.length - 5)), keeping the last 5. This caps each panel at 5 stacked layers so the DOM doesn't grow unbounded while preserving a few layers behind the newest reveal.

Init — on DOMContentLoaded
document.addEventListener("click", handleSlider);
updateImages(2);        // reveal img2 into both panels on load
updateActiveSlide();    // highlight the title at index 1 ("Arcadian Complex")
The title↔image offset (important)

The active title index and the image number are intentionally off by one: the code reveals updateImages(currentIndex + 1) while highlighting title index currentIndex. On load (currentIndex = 1) it shows img2 and highlights index 1. Through the click loop the image number runs img2 → img3 → img4 → img5 → img6 → img7 → img8 → (wrap) → img2 …. So img1 is a spare that the default click cycle never reaches — only images 2–8 appear. Preserve this mapping.

Assets / images

  • 8 full-bleed architecture photographs named img1.jpg … img8.jpg under

/c/modular-slider/. Mostly portrait ~3:4 (a couple are square 1:1); each fills the 550×500 near-square panel box via object-fit: cover, so exact ratio is not critical.

  • Theme: modern / futuristic residential and civic architecture, editorial and atmospheric,

shot in golden-hour, misty-forest, coastal-dune and mountain settings — a palette that sits well on the near-black stage at 50% opacity. Generic by role and form, no brand marks, logos or baked-in text:

  1. Cantilevered beach house — a modern two-story home clad in light wood over a dark base,

glass railings and floor-to-ceiling windows, perched on grassy coastal sand dunes at golden hour. *(the spare image the click cycle never reaches)*

  1. Glowing forest cabin — a warmly lit angular cabin with a steep mono-slope roof and a

full-height glass wall, orange interior glow with sofa and plants, in a misty green forest at dusk. *(first image shown on load)*

  1. Rammed-earth house — a minimalist two-story terracotta-toned home with vertical wood-slat

screens and a large recessed window, dappled sun, warm beige monochrome.

  1. Timber tower home — a three-story timber-clad house with tall black-framed vertical

windows reflecting autumn trees, raised concrete base, stone path through a green garden under blue sky.

  1. Curved pod cabin — a futuristic rounded wooden pod with a big glass front, cantilevered

over rippled sand dunes and beach grass under a bright blue sky.

  1. Faceted villa — a sculptural angular villa with a sweeping faceted dark roof and a large

triangular glass facade, on a green lawn with a curving path amid forested mountains.

  1. Brutalist arches — a monumental organic concrete building with flowing arched supports and

rounded window openings, small figures at the base, warm sunset light over a forested valley.

  1. Stacked hillside house — a split-level modern home of pale wood volumes with big glass

corners and balconies, built into a grassy hillside with a dirt path under clear blue sky.

Behavior notes

  • Interaction is a single global click — the entire document advances the slider; there are no

arrows or dots. It does not autoplay.

  • Loop: the 7-state index wrap plus the two duplicated trailing titles make the strip feel

continuous; on wrap (7 → 1) the strip snaps its transform back toward x = 0 for the next cycle.

  • Responsive (max-width: 900px): the image stage becomes full-bleed (.slide-images

width: 100%; height: 100%), the split-panel and hover clip-paths flatten to a full rectangle (polygon(0 0, 100% 0, 100% 100%, 0 100%)), the title h1 drops to 24px and only the active title is visible (.title h1 { opacity: 0 }, .active h1 { opacity: 1 }, using the 0.25s opacity transition).

  • Hover widening is pure CSS (1s ease-out-circ) and independent of the click-driven GSAP

tweens.

  • No reduced-motion branch in the original; keep the motion GSAP-driven and lightweight (two

concurrent tweens per click, DOM capped at 5 image layers per panel).