All components

Exo Ape Infinite Carousel

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

Open live demo ↗ Raw prompt (.md)

What it does

A full-screen infinite image carousel driven by wheel and touch input: each scroll builds a fresh slide and reveals it with a GSAP timeline that wipes a full-bleed background via animated clip-path polygons, scales the outgoing image to 1.5, and slides a centered portrait thumbnail plus masked title, description and counter text in the scroll direction. All tweens share a 1.25s duration and a custom '.87,0,.13,1' CustomEase, with a one-second cooldown between slides.

How it's built

Categoryslider
Techgsap
GSAP pluginsCustomEase
Complexitypage
Performance costlight
Mobile-safeyes

carousel slider clip-path customease wheel infinite fullscreen cinematic masked-text

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

Full-screen Infinite Carousel with Clip-path Wipe Transitions

Goal

Build a full-screen, infinitely looping image carousel driven by mouse wheel and touch swipes. Every scroll step spawns a brand-new slide and reveals it with a synchronized GSAP timeline: the full-bleed background image wipes in via an animated clip-path polygon (from the bottom when scrolling down, from the top when scrolling up) while the outgoing background zooms to 1.5x behind it; simultaneously a centered portrait thumbnail wipes in the opposite vertical direction with an inner-image parallax, and the title, description and slide counter slide out/in through clipped text masks. All tweens share one 1.25s duration and one custom cubic-bezier ease, so the whole transition feels like a single cinematic wipe.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugin CustomEase (import { CustomEase } from "gsap/CustomEase" and gsap.registerPlugin(CustomEase)). No other libraries — no Lenis, no ScrollTrigger. The page never actually scrolls; wheel/touch events are intercepted and hijacked.

Layout / HTML

Body contains, in order:

  1. <nav> — fixed overlay bar at the top:
  2. .logo with a <p> wordmark (e.g. "Motionprompts").
  3. .nav-items with four <p> links: "Work", "Studio", "News", "Contact".
  4. <footer> — fixed overlay bar at the bottom:
  5. a <p> reading "All Projects" on the left.
  6. .slider-counter on the right containing three children in a row: .count (a <div> holding a <p>1</p> — this is the animated current-slide digit), a <p>/</p>, and a <p>7</p> (the static total).
  7. .slider — the full-viewport stage. Initially contains:
  8. one .slide > .slide-bg-img > <img> (full-bleed background, slide 1 image).
  9. .slide-main-img > .slide-main-img-wrapper > <img> (the centered portrait thumbnail, same slide 1 image).
  10. .slide-copy > .slide-title (with <h1>Field Unit</h1>) and .slide-description (with <p>Concept Art</p>).

The JS clones/creates new .slide, .slide-main-img-wrapper, <h1>, <p> and counter <p> nodes on every transition and removes the old ones when the timeline completes, so the DOM always ends up with exactly one of each.

Store slide data in two JS arrays (index 0 = slide 1):

  • Titles: "Field Unit", "Astral Convergence", "Eclipse Core", "Luminous", "Serenity", "Nebula Point", "Horizon".
  • Descriptions: "Concept Art", "Soundscape", "Experimental Film", "Editorial", "Music Video", "VFX", "Set Design".

Image paths follow the pattern ./img${n}.jpeg with n from 1 to 7 (7 slides total, totalSlides = 7).

Styling

  • Global reset (* { margin:0; padding:0; box-sizing:border-box }). html, body are width/height: 100%; font family "TWK Lausanne", falling back to sans-serif. All UI text is white (#fff).
  • All <img>: width/height: 100%; object-fit: cover; will-change: transform.
  • nav and footer: position: fixed, full 100vw width, padding: 3em, display: flex; justify-content: space-between; align-items: center; z-index: 2. Their <p> text is 15px, font-weight: lighter. .nav-items is a flex row with gap: 2em.
  • .slider-counter: flex row; each <p> and the .count div are 24px wide with centered content; the / and total <p>s are opacity: 0.35 while the current digit is full opacity 16px, line-height: 1.
  • .count: position: relative; height: 18px with clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) — it acts as an 18px-tall mask for the vertically-sliding digit. Its <p> is absolutely positioned with will-change: transform.
  • .slider: position: relative; width: 100vw; height: 100vh; overflow: hidden.
  • .slide: absolutely positioned, inset 0, full size.
  • .slide-bg-img: absolutely positioned full size, clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) (full rectangle), will-change: clip-path. It has an ::after overlay covering it entirely with background-color: rgba(0,0,0,0.125) to slightly darken the photo behind the white text.
  • .slide-main-img: absolutely centered (top: 50%; left: 50%; transform: translate(-50%,-50%)), width: 25%; height: 50% of the viewport, z-index: 2 — a portrait-ish window floating over the background.
  • .slide-main-img-wrapper: absolute, full size of its parent, full-rectangle clip-path like above, will-change: clip-path.
  • .slide-copy: absolutely positioned at top: 50%; left: 30%; transform: translate(-50%,-50%), white, z-index: 2 — so the text block sits left of the centered thumbnail.
  • .slide-title: position: relative; width: 500px; height: 50px; margin-bottom: 0.75em with the full-rectangle clip-path (a 50px-tall text mask). Its <h1> is absolute, 48px, font-weight: 400, line-height: 1, will-change: transform.
  • .slide-description: same pattern but height: 20px; its <p> is absolute, 18px, font-weight: lighter, line-height: 1.
  • Media query max-width: 900px: .slide-main-img widens to 75% and .slide-copy moves to top: 60%; left: 60%.

GSAP effect (the important part — be exact)

Ease and timing. Every single tween uses duration: 1.25 and ease: CustomEase.create("", ".87,0,.13,1") (a steep symmetric bezier — slow start, fast middle, slow settle). All tweens are placed at position 0 of one gsap.timeline(), so everything moves in perfect sync.

State machine. Track currentSlide (starts at 1), isAnimating, scrollAllowed (starts true) and lastScrollTime. A transition may only start if not animating, scrolling is allowed, and at least 1000ms have passed since the last accepted scroll. Infinite wrap: scrolling down past slide 7 goes to 1; scrolling up past 1 goes to 7.

Building the incoming elements (before the timeline runs). For direction "down" (next slide) / "up" (previous):

  1. New .slide > .slide-bg-img > <img> appended to .slider. Its .slide-bg-img starts fully collapsed via inline clip-path:
  2. down: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%) (zero-height sliver at the bottom edge).
  3. up: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%) (zero-height sliver at the top edge).
  4. New .slide-main-img-wrapper > <img> appended inside .slide-main-img. It starts collapsed the opposite way:
  5. down: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%) (collapsed at top).
  6. up: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%) (collapsed at bottom).

Its inner <img> is pre-offset with gsap.set to y: "-50%" (down) or y: "50%" (up) for a parallax reveal.

  1. New <h1> (title), <p> (description) and counter <p> (the digit) appended into .slide-title, .slide-description and .count respectively, pre-offset with gsap.set:
  2. title: y: 50 (down) / y: -50 (up).
  3. description: y: 20 / y: -20.
  4. counter digit: y: 18 / y: -18.

(Each offset equals its clipped container's height, so the incoming text waits just outside the mask.)

The timeline (all tweens at position 0, each 1.25s with the CustomEase above):

  1. New .slide-bg-img clip-path expands to a full rectangle:
  2. down: to polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%) (wipes upward from the bottom edge).
  3. up: to polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) (wipes downward from the top edge).
  4. The outgoing slide's background <img> scales to scale: 1.5 (cinematic push-in behind the wipe).
  5. New .slide-main-img-wrapper clip-path expands to full:
  6. down: to polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%).
  7. up: to polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%).
  8. Outgoing wrapper's <img> parallaxes out: y: "50%" (down) / y: "-50%" (up).
  9. Incoming wrapper's <img> parallaxes to y: "0%".
  10. Outgoing <h1> to y: -50 (down) / y: 50 (up); incoming <h1> to y: 0.
  11. Outgoing description <p> to y: -20 / y: 20; incoming to y: 0.
  12. Outgoing counter digit to y: -18 / y: 18; incoming to y: 0.

So on scroll-down everything (text and thumbnail image) flows upward while the background wipe rises from the bottom; on scroll-up every direction is mirrored.

Cleanup. The timeline's onComplete removes the old .slide, old .slide-main-img-wrapper, old <h1>, old description <p> and old counter <p>, sets isAnimating = false, and after a setTimeout of 100ms re-enables scrollAllowed and refreshes lastScrollTime.

Input handling.

  • wheel listener on window with { passive: false }, calling e.preventDefault(); deltaY > 0 means "down". Route through a handleScroll(direction) guard that enforces the flags plus the 1000ms cooldown.
  • Touch: on touchstart record touches[0].clientY and set a touch-active flag. On touchmove (passive: false, preventDefault()), once the vertical delta exceeds 10px, clear the flag and trigger handleScroll — positive delta (finger moved up) = "down". touchend clears the flag.

Assets / images

7 photographs, one per slide, each used twice in a slide (full-bleed background AND the centered thumbnail — always the same image for both). They read as a moody, cinematic portfolio set (landscape orientation works; they are cropped with object-fit: cover):

  1. Two figures in white clothing walking away down a softly lit cream-toned corridor tunnel.
  2. A crescent-moon-shaped sculpture with a small lizard resting on it against a warm textured wall.
  3. A large translucent draped plastic sheet billowing on a frame in a desert at golden sunset.
  4. A 3D-render portrait of a red-haired freckled woman in an ornate white hood encrusted with crystals.
  5. A lone faceless figure in white walking through a vast curved concrete tunnel, soft cinematic light.
  6. An abstract macro of an organic white-and-black porous surface with rounded cavities.
  7. A person in a red robe standing before enormous red drapes hung between sandstone desert cliffs.

Behavior notes

  • The carousel is infinite in both directions (wraps 7 → 1 and 1 → 7).
  • The page itself never scrolls; wheel and touchmove are fully prevented.
  • Rapid scrolling is throttled: one transition per ~1s minimum (1000ms cooldown + 100ms post-animation delay), and re-entry is blocked while a timeline is running.
  • Works on touch devices (swipe up = next, swipe down = previous) and adapts below 900px viewport width (larger thumbnail, repositioned copy).
  • Wrap all setup in a DOMContentLoaded listener.