All components

Karim Saab Fashion Carousel

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

Open live demo ↗ Raw prompt (.md)

What it does

A full-screen fashion carousel where a wheel or touch swipe triggers a GSAP transition: the current slide scales down, rotates and flies off while the incoming slide slides in from an animated clip-path polygon; SplitText then reveals the title words and the description, tags, index and link lines with staggered power4 masked upward moves.

How it's built

Categoryslider
Techgsap
GSAP pluginsSplitText
Complexitypage
Performance costlight
Mobile-safeyes

carousel fullscreen gsap splittext clip-path wheel text-reveal editorial fashion

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 Work Carousel — Fly-Off Exit + Clip-Path Entry + SplitText Reveals

Goal

Build a full-screen, scroll-hijacked work/portfolio carousel (one slide visible at a time). On wheel or touch swipe, the current slide shrinks, rotates and flies off screen while the next slide enters from the opposite edge inside an animated clip-path polygon that expands to full screen; then the slide's title words and every text line (description, tags, index, link) reveal upward through SplitText masks with staggered power4 eases.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugin SplitText (import { SplitText } from "gsap/SplitText"). No other libraries. Do NOT register ScrollTrigger — scrolling is fully hijacked with native wheel/touch listeners.

Data

Create a slides.js module that default-exports an array of 4 slide objects, each with:

  • slideTitle (string, e.g. "Second Skin", "Half Light", "Sharp Shoulder", "Under Veil")
  • slideDescription (one sentence, ~20 words, editorial tone about the project)
  • slideUrl (e.g. /work/second-skin)
  • slideTags (array of 4 short tags, e.g. ["Leather", "Studio", "Still life", "AW25"])
  • slideImg (path to the slide's full-bleed image)

Layout / HTML

<body> contains only:

.slider                     ← fixed-size viewport container
  .slide                    ← the FIRST slide, hard-coded in the HTML (matches slides[0])
    .slide-img > img        ← full-bleed image
    .slide-header
      .slide-title > h1     ← project title
      .slide-description > p
      .slide-link > a       ← text "View Project", href = slideUrl
    .slide-info
      .slide-tags           ← <p>Tags</p> followed by one <p> per tag
      .slide-index-wrapper  ← <p id="slide-index">01</p> <p>/</p> <p id="total-slide-count">04</p>

All subsequent slides are built entirely in JS with document.createElement, reproducing this exact structure (the index <p> shows the current slide number zero-padded to 2 digits, e.g. 02, and the total count 04).

Styling

  • Google Fonts: DM Sans (body/headings) and DM Mono (all small text). Global reset (* { margin:0; padding:0; box-sizing:border-box }).
  • img { width:100%; height:100%; object-fit:cover }.
  • All h1, p, a: text-transform: uppercase; color: #fff. Links have no underline.
  • h1: DM Sans, font-size: 5rem, font-weight: 600, letter-spacing: -0.1rem.
  • p, a: DM Mono, font-size: 0.9rem, font-weight: 500, letter-spacing: -0.01rem.
  • .slider: position: relative; width: 100vw; height: 100svh; background-color: #000; overflow: hidden.
  • .slide and .slide-img: position: absolute; top: 0; left: 0; width: 100vw; height: 100svh. .slide { will-change: transform }.
  • .slide-header: absolute, bottom: 2rem; left: 50%; transform: translate(-50%, 0%), width: 75%, centered text, flex column, align-items: center; gap: 1rem; z-index: 1.
  • .slide-description: width: 60%; text-align: center; margin-bottom: 1rem.
  • .slide-info: absolute, left: 0; bottom: 2rem; width: 100vw; padding: 0 2rem, flex justify-content: space-between; align-items: flex-end.
  • .slide-tags: flex column; its first <p> ("Tags") has margin-bottom: 1rem.
  • .slide-index-wrapper: flex row; each <p> inside is width: 2rem; text-align: center.
  • .line, .word (SplitText output): position: relative; display: inline-block; will-change: transform.
  • @media (max-width: 1000px): h1 { font-size: 2rem; letter-spacing: 0 }, p { font-size: 0.8rem }, .slide-header { top: 50%; bottom: unset; transform: translate(-50%, -50%); width: 90% }, .slide-description { width: 100% }.

GSAP effect (be exact)

Everything runs inside DOMContentLoaded. State: currentSlide = 1, isAnimating = false, scrollAllowed = true, lastScrollTime = 0, totalSlides = slides.length.

Input → trigger
  • window wheel listener with { passive: false } + e.preventDefault(); direction = e.deltaY > 0 ? "down" : "up".
  • Touch: on touchstart record touchStartY and set isTouchActive = true; on touchmove ({ passive: false }, preventDefault()), if the vertical delta exceeds 50px, set isTouchActive = false and fire with direction = delta > 0 ? "down" : "up"; touchend resets isTouchActive.
  • A handleScroll(direction) gate ignores input while isAnimating || !scrollAllowed, and also throttles: ignore if Date.now() - lastScrollTime < 1000 ms; otherwise update lastScrollTime and run the transition.
Slide counter (infinite wrap)

direction === "down"currentSlide = currentSlide === totalSlides ? 1 : currentSlide + 1; "up" → wrap the other way.

Direction-dependent values
  • Exit translate: exitY = direction === "down" ? "-200vh" : "200vh".
  • Entry start translate: entryY = direction === "down" ? "100vh" : "-100vh" (new slide comes from the bottom when scrolling down).
  • Entry start clip-path:
  • down: polygon(20% 20%, 80% 20%, 80% 100%, 20% 100%) (a 60%-wide window anchored to the bottom edge)
  • up: polygon(20% 0%, 80% 0%, 80% 80%, 20% 80%) (anchored to the top edge)
1) Exit tween (current slide)

gsap.to(currentSlideElement, { scale: 0.25, opacity: 0, rotation: 30, y: exitY, duration: 2, ease: "power4.inOut", force3D: true, onComplete: () => currentSlideElement.remove() }) — the slide simultaneously shrinks to a quarter size, fades out, tilts 30° clockwise and flies two viewport-heights off screen.

2) Entry (new slide) — starts after a 750 ms setTimeout
  1. Build the new slide element from the data, then gsap.set(newSlide, { y: entryY, clipPath: entryClipPath, force3D: true }) and append it to .slider.
  2. Run SplitText on it:
  3. Title h1: SplitText.create(h1, { type: "words", wordsClass: "word", mask: "words" }).
  4. Every p and a in the slide: SplitText.create(el, { type: "lines", linesClass: "line", mask: "lines", reduceWhiteSpace: false }).
  5. gsap.set([all .word, all .line], { y: "100%", force3D: true }) — all text starts hidden below its mask.
  6. Container tween: gsap.to(newSlide, { y: 0, clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", duration: 1.5, ease: "power4.out", force3D: true }) — the slide travels to center while the polygon window expands to the full rectangle.
  7. In that tween's onStart, build a gsap.timeline() for the text reveals (all y: "100%""0%", all ease: "power4.out", all duration: 1):
  8. .slide-title .word with stagger: 0.1, placed at absolute time 0.75 on the timeline.
  9. .slide-tags .line with stagger: 0.1, position "-=0.75" (overlaps the title reveal).
  10. .slide-index-wrapper .line with stagger: 0.1, position "<" (same start as tags).
  11. .slide-description .line with stagger: 0.1, position "<".
  12. .slide-link .line (no stagger), position "-=1".
  13. In the container tween's onComplete: isAnimating = false, and after a 100 ms timeout set scrollAllowed = true and lastScrollTime = Date.now().

Net result: exit and entry overlap (exit lasts 2 s, entry starts at 0.75 s), so for a moment both slides are moving in opposite directions.

Assets / images

4 full-bleed, cinematic fashion/editorial portraits (moody studio lighting, futuristic styling), one per slide, displayed full-screen with object-fit: cover — landscape ~16:9 or larger works best. Each slide pairs its image with its title/description/tags. No logos or brand marks.

Behavior notes

  • The first slide is static HTML and its text is NOT split — SplitText only runs on slides created during transitions.
  • The carousel loops infinitely in both directions.
  • Native page scroll never happens: the page is exactly one viewport tall and wheel/touch events are prevented.
  • Works on touch devices via the 50px swipe threshold; the responsive breakpoint at 1000px recenters the header vertically.