All components

Aristide Benoist Slider

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

Open live demo ↗ Raw prompt (.md)

What it does

A full-screen horizontal image carousel where clicking a segment in the top progress-bar nav slides the track with gsap.to and a custom 'hop' CustomEase, tweens the background overlay to a random color, and re-animates two rows of large display-type title letters sliding in with power2.out. Triggered by click on the nav segments.

How it's built

Categoryslider
Techgsap
GSAP pluginsCustomEase
Complexitypage
Performance costmedium
Mobile-safeyes

horizontal-slider carousel custom-ease gsap kinetic-typography click-navigation editorial fullscreen portfolio

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

Fullscreen Horizontal Slider — Segmented Progress Nav + Kinetic Display-Type Titles

Goal

Build a full-screen horizontal image carousel with a signature choreography driven entirely by clicking segments in a top progress-bar navigation. Each slide holds one centered image. Above them sits a slim horizontal bar split into 30 thin segments; the active segment stretches wide (a pure-CSS flex grow) while the others stay hairline-thin. Clicking a different segment does three things at once, all 1.5s long on a custom "hop" CustomEase: (1) the whole horizontal track of slides slides sideways to the chosen slide, (2) a full-screen background-color overlay tweens to a new random color, and (3) a giant two-row display-type title re-animates — every letter is destroyed and re-created, then slides in from the side (from the right when advancing, from the left when going back) with power2.out. It's an editorial portfolio slider: oversized serif kinetic typography floating over a color wash, with a centered image per slide.

Tech

  • Vanilla HTML / CSS / JS with ES module imports, bundled by a Vite-style dev server (npm project).
  • gsap (npm) plus exactly one GSAP plugin: CustomEase. No ScrollTrigger, no SplitText, no smooth-scroll library, no canvas/WebGL. The whole thing is click-driven — there is no scroll and no rAF loop.
  • Imports and registration:
import gsap from "gsap";
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(CustomEase);
CustomEase.create("hop", "M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1");
  • Title copy lives in a separate data.js (export const titles = [...]). All DOM building + wiring happens inside a single document.addEventListener("DOMContentLoaded", …) in script.js (<script type="module" src="./script.js">).

Layout / HTML

The markup is mostly empty shells that JS fills. Class names are load-bearing. Use neutral/fictional copy — no real brand names. The demo wordmark is "Motionprompts".

<div class="container">
  <nav>
    <a href="#" id="logo">Motionprompts</a>
    <a href="#">Subscribe</a>
  </nav>
  <footer>
    <a href="#">Unlock Source Code with PRO</a>
    <a href="#">Link in description</a>
  </footer>

  <div class="bg-overlay"></div>     <!-- full-screen color wash -->
  <div class="slider-nav"></div>     <!-- JS injects 30 .nav-item-wrapper segments -->
  <div class="slides"></div>         <!-- JS injects 30 .slide elements in a horizontal flex row -->

  <div class="slide-title">
    <div class="slide-title-row"><!-- 7 empty .letter divs --></div>
    <div class="slide-title-row"><!-- 7 empty .letter divs --></div>
  </div>
</div>

<script type="module" src="./script.js"></script>

The two .slide-title-row divs each contain exactly 7 empty <div class="letter"> in the HTML (14 letter slots total). JS injects/replaces a <span> inside each on every title change.

Per-slide DOM (built in the JS loop, i = 0..29):

.slide            (flex 1, one viewport wide)
  .img            (centered box, 50% × 50% of viewport)
    img           src = /c/aristidebenoist-slider/img{i+1}.jpg

Per-nav-segment DOM (built in the same loop):

.nav-item-wrapper   (segment 0 also gets class "active")
  .nav-item         (the thin tick inside)

Styling

Global reset: * { margin:0; padding:0; box-sizing:border-box; }. html, body { width:100%; height:100%; }.

Fonts (name these families; supply free substitutes if unavailable):

  • Body / UI: "PP Neue Montreal" — a neutral grotesque sans (substitute: Inter / Neue Haas Grotesk).
  • Display (logo + the giant title letters): "Timmons NY 2.005" — a tall, high-contrast condensed serif display face (substitute: a heavy fashion-editorial serif like a condensed Didone / GT Sectra display / Ogg). Set on nav a#logo and .letter span.

Colors / tokens:

  • Text is black #000.
  • .bg-overlay initial background: rgb(213, 183, 71) (a mustard/gold), with filter: brightness(0.75); and opacity: 0.5; — a soft muted color wash over the whole viewport. Later JS tweens its background-color to random hex values.

Structure:

  • .container: width:100vw; height:100vh; overflow:hidden; — clips the oversized horizontal track and the giant letters.
  • nav, footer: position:fixed; left:0; width:100vw; padding:2.75em; display:flex; justify-content:space-between; z-index:2;nav{top:0}, footer{bottom:0}.
  • a: text-decoration:none; text-transform:uppercase; font-size:12px; font-weight:500; color:#000;.
  • nav a#logo: position:relative; top:-12px; display serif, font-size:42px;.
  • .bg-overlay: position:fixed; inset:0; width:100vw; height:100vh; (see color tokens above).

The segmented progress nav (the click target):

  • .slider-nav: position:fixed; top:5%; left:50%; transform:translateX(-50%); width:25%; height:15px; display:flex; justify-content:space-between; z-index:10;.
  • .nav-item-wrapper: flex:1; height:100%; display:flex; justify-content:center; align-items:center; transition: all 750ms cubic-bezier(0, 0.75, 0.5, 1);CSS transition, not GSAP.
  • .nav-item: width:1px; height:100%; border:1px solid rgba(0,0,0,0.15); transition: all 750ms cubic-bezier(0, 0.75, 0.5, 1); — a faint hairline tick.
  • .nav-item-wrapper.active: flex:5; — the active segment claims 5× the width of the others, so the bar reads like a progress indicator with one long lit cell.
  • .nav-item-wrapper.active .nav-item: width:50%; border:1px solid rgba(0,0,0,1); — the active tick grows and turns solid black.

The horizontal slide track:

  • .slides: position:fixed; top:0; left:0; width:3000vw; height:100vh; display:flex;3000vw = 30 slides × 100vw laid side by side. GSAP translates this whole element on x.
  • .slide: flex:1; width:100vw; height:100vh; display:flex; justify-content:center; align-items:center;.
  • .slide .img: width:50%; height:50%; opacity:0.75; — image sits in a centered box occupying half the viewport each way.
  • img: width:100%; height:100%; object-fit:cover;.

The giant kinetic title:

  • .slide-title: position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:75%; height:65%; display:flex; flex-direction:column; pointer-events:none; — floats centered over the slides, non-interactive so nav clicks pass through.
  • .slide-title-row: flex:1; width:100%; display:flex; gap:0; (two rows).
  • .slide-title-row:nth-child(2): position:relative; left:4em; — the second row is nudged right for an offset editorial layout.
  • .letter: flex:1; height:100%; padding-left:2em; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); — each of the 7 cells per row is an equal-width column; the full-rectangle clip-path masks the span so a letter that slides in from the side is clipped at the cell edges (it wipes in rather than flying across).
  • .letter span: position:relative; display:inline-block; font-family:"Timmons NY 2.005"; font-size:280px; filter: brightness(0.25) saturate(0.75) !important; — NOTE: this !important filter overrides the inline filter GSAP sets, so every letter renders as a dark, desaturated version of the current color (near-black), regardless of the random color assigned to it.

GSAP effect (be exact)

The "hop" CustomEase

CustomEase.create("hop", "M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1") — a fast-out, long-settle ease: it accelerates hard in the first ~30% then glides slowly into the end. Used for both the slide-track slide and the color tween.

Build loop (30 items, index i = 0..29)

For each i:

  1. Create .nav-item-wrapper (+ .nav-item child); segment 0 gets class active. Append to .slider-nav.
  2. Create .slide (+ .img + <img src="/c/aristidebenoist-slider/img{i+1}.jpg">); slide 0 also gets class active. Append to .slides.
  3. Attach a click handler to the wrapper (closure captures its own i via let).

Track two module-level values: numberOfItems = 30 and currentIndex = 0.

Click handler on a nav segment i
if (i === currentIndex) return;                    // ignore clicks on the current segment

// active class swap (drives the CSS width transition on the bar)
document.querySelectorAll(".nav-item-wrapper").forEach(n => n.classList.remove("active"));
thisWrapper.classList.add("active");

// 1) slide the whole track horizontally
gsap.to(".slides", { x: `${-i * 100}vw`, duration: 1.5, ease: "hop" });

// 2) tween the background wash to a fresh random hex color
const newColor = getRandomColor();                 // "#" + 6 random hex chars
gsap.to(".bg-overlay", { backgroundColor: newColor, duration: 1.5, ease: "hop" });

// 3) re-animate the title (see below), then commit the index
updateTitle(i, newColor);
currentIndex = i;
  • getRandomColor(): builds "#" + 6 characters picked at random from 0123456789ABCDEF.
  • The horizontal target is **x: "-i*100vw"** (e.g. clicking segment 7 → x: "-700vw"), so the chosen slide lands centered in the viewport.
updateTitle(newIndex, color) — the kinetic letters

titles[newIndex] is [rowArray0, rowArray1], each row a 7-element array of single characters (many are empty strings "", which create gaps → the scattered word layout). For each of the 2 .slide-title-rows, for each of its 7 .letter cells:

const direction = newIndex > currentIndex ? 150 : -150;   // forward → enter from right; back → from left
// remove the old <span> if present
const span = document.createElement("span");
gsap.set(span, { x: direction, color: color, filter: "brightness(0.75)" });
span.textContent = title[rowIndex][letterIndex] || "";     // may be an empty string
letter.appendChild(span);
gsap.to(span, { x: 0, duration: 1, ease: "power2.out", delay: 0.125 });

Key points:

  • Direction is decided before currentIndex is updated, so it reflects travel direction: advancing to a higher index → letters start at x: 150 (enter from the right); going back → x: -150 (enter from the left).
  • Every letter uses the same duration: 1, ease: "power2.out", delay: 0.125no stagger; all 14 letters glide in together, clipped by their .letter cell so they appear to wipe in from the edge.
  • The color set inline is the same random color used for the wash, but the CSS filter … !important on .letter span overrides the inline filter, so letters render near-black/desaturated regardless.
Initial state (on load, after the build loop)

Call updateTitle(0, getComputedStyle(bgOverlay).backgroundColor) once. Since newIndex (0) is not > currentIndex (0), direction = -150, so the very first title wipes in from the left. Slide 0 and nav segment 0 start active; the track sits at x: 0.

Title data (data.js)

export const titles is an array of 30 entries, each [ [7 chars], [7 chars] ]. Empty strings are intentional gaps. Use short, neutral invented words (no real brands). Reproduce this exact set for a faithful result:

export const titles = [
  [["p","","r","","i","s","m"], ["","t","","o","n","e",""]],
  [["","l","u","m","e","","n"], ["d","","r","e","a","m",""]],
  [["r","","u","s","","h",""], ["","s","l","i","","c","e"]],
  [["e","","c","h","o","","e"], ["","c","o","","d","e","6"]],
  [["t","e","","c","h","y",""], ["","m","","","a","p","l"]],
  [["","w","a","v","","e","s"], ["b","o","","x","","",""]],
  [["c","","u","b","","","e"], ["","s","i","t","","9","0"]],
  [["r","u","","s","h","","x"], ["t","","o","r","","k",""]],
  [["c","","o","d","","e",""], ["l","a","b","","","0","8"]],
  [["m","i","x","","e","","d"], ["","","a","r","","k",""]],
  [["","t","e","","s","t",""], ["b","","e","d","","5","4"]],
  [["f","o","c","u","","","s"], ["","d","o","c","k","",""]],
  [["p","","a","","c","e",""], ["s","e","t","","1","","7"]],
  [["","b","","l","a","s","t"], ["m","o","","d","","","e"]],
  [["z","o","","n","e","",""], ["g","e","3","","","n",""]],
  [["d","","r","e","a","","m"], ["s","c","a","p","0","",""]],
  [["e","l","e","v","a","n",""], ["","p","a","","t","","h"]],
  [["","s","","h","i","f","t"], ["","n","e","","","u","e"]],
  [["i","","c","o","","","n"], ["","m","e","m","o","",""]],
  [["","a","","","u","r","a"], ["w","","a","v","e","","6"]],
  [["s","t","e","l","","l","a"], ["","o","","r","b","i","t"]],
  [["v","","e","r","t","e",""], ["c","o","","r","","e",""]],
  [["i","n","f","i","","9",""], ["","","","e","t","h","o"]],
  [["","","q","u","a","n","t"], ["d","e","","c","","","k"]],
  [["","n","","","o","v","a"], ["r","","a","y","","",""]],
  [["","r","a","d","i","a","n"], ["g","l","o","","","w","0"]],
  [["c","o","s","m","i","c",""], ["p","","a","t","h","",""]],
  [["","s","o","l","a","r",""], ["d","r","i","f","","","t"]],
  [["z","e","n","","l","a","y"], ["","e","r","v","","y",""]],
  [["a","p","e","","x","",""], ["f","o","r","g","e","0","0"]],
  [["","c","r","y","s","t","a"], ["l","b","y","t","e","",""]],
];

Assets / images

30 slide images at /c/aristidebenoist-slider/img1.jpg … img30.jpg. Each is shown in a centered box occupying 50% × 50% of the viewport with object-fit: cover at opacity: 0.75, so any aspect ratio works (roughly square-to-landscape framing reads best; crops are inevitable). Content is an eclectic editorial mix — extreme close-up portraits, moody studio product shots, abstract 3D renders (glossy discs, spheres, stacked panels), gadgets/devices, and interiors. Subjects need not relate; the point is variety across the 30 slides. No brand marks or logos in the images.

Behavior notes

  • Click-only, no scroll, no rAF. The only interactions are clicks on the 30 nav segments. Clicking the already-active segment is a no-op.
  • Responsive (max-width: 900px): .slider-nav widens to width:40%; .slide .img grows to width:80%; height:75%; .slide-title becomes left:47.5%; height:25% and its second row resets to left:0; .letter span font-size drops from 280px to 100px.
  • The bar's segment-widening is a CSS transition (750ms cubic-bezier(0,0.75,0.5,1)), intentionally slightly out of sync with the 1.5s GSAP "hop" slide/color tweens.
  • Colors are random each click, so no two sessions look identical; only the letter darkening (filter … !important) keeps the type readable over the shifting wash.