All components

Split Column Infinite Slider

JavaScript animation component · Published 2026-09-05 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Two full-height columns driven by a virtual wheel or touch scroll: each project is a pair of photos, the left one revealed by a clip-path rising from the bottom and the right one descending from the top, with the images drifting in opposite directions and a single 100vw title block split across both columns that holds still while its slide is centred. Slides are created and removed around the current index with modulo wrapping, so the loop is infinite, and a light bar tracks progress.

How it's built

Categoryslider
Techvanilla JS
Complexitysection
Performance costlight
Mobile-safeyes

slider infinite split clip-path wheel raf parallax glass

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

Split Column Infinite Slider — Virtual Wheel Scroll, Opposite Clip-Path Reveals & Split Titles

Goal

Build a fullscreen two-column project slider driven by a virtual scroll (wheel or touch drag, lerped). Each project is a pair of photos: the left one is revealed by a clip-path rising from the bottom, the right one by a clip-path descending from the top, while their images drift vertically in opposite directions (with a fixed zoom to hide the drift) and a single 100vw-wide text block is split across both columns — half the title in each — that holds still while its slide is centred and drifts a little on the way in and out. Slides are created and removed around the current index with modulo wrapping, so the loop is infinite in both directions. A thin light bar on the right shows progress through the set. No GSAP: per-frame arithmetic.

Tech

Vanilla HTML/CSS/JS, one module. No libraries. Font: Inter (500–800).

Layout / HTML

<div class="sc">
  <section class="sc-slider" aria-label="Projects">
    <div class="sc-column sc-column--left"></div>
    <div class="sc-column sc-column--right"></div>
    <div class="sc-progress" aria-hidden="true"><span class="sc-progress__bar"></span></div>
  </section>
</div>

Each slide created by JS:

<div class="sc-slide" style="z-index:…">
  <img src="{side}_{n}.jpg" alt="" />
  <div class="sc-slide__shade"></div>
  <div class="sc-copy">
    <ul class="sc-copy__tags"><li>…</li><li>…</li><li>…</li></ul>
    <h2 class="sc-copy__title">Northline</h2>
    <a class="sc-copy__link" href="#">View project</a>
  </div>
</div>

Data (5 projects): Northline · Tidewater · Dune Route · Night Field · Concrete Hours, each with three short tags.

Styling

:root: --sc-bg:#0a0b0d, --sc-ink:#f6f5f2, --sc-line:rgba(255,255,255,.22), --sc-shade:.45.

  • .sc-sliderposition:relative; width:100%; height:100svh; display:flex; overflow:hidden. .sc-columnflex:1; position:relative; height:100%; overflow:hidden.
  • .sc-slideposition:absolute; inset:0; overflow:hidden; img object-fit:cover; will-change:transform. .sc-slide__shadeposition:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,calc(var(--sc-shade)*.6)), rgba(0,0,0,var(--sc-shade)) 60%, rgba(0,0,0,calc(var(--sc-shade)*1.3))).
  • .sc-copyposition:absolute; top:0; width:100vw; height:100%; pointer-events:none; left column left:0, right column right:0 — that is the whole trick: each column shows its half of the same 100vw block.
  • .sc-copy__title — centred (top:50%; left:50%; translate(-50%,-50%)), uppercase, clamp(2.5rem,10vw,15rem), weight 800, letter-spacing:-.03em; line-height:1; white-space:nowrap.
  • .sc-copy__tags — at top:31%, centred, display:flex; gap:.5rem; each li is a glass chip (padding:.45em .9em; border-radius:999px; border:1px solid var(--sc-line); background:rgba(255,255,255,.08); backdrop-filter:blur(12px); font-size:.72rem; weight 500; letter-spacing:.06em; uppercase).
  • .sc-copy__link — at top:64%, centred, same glass pill at rgba(255,255,255,.1), weight 600, pointer-events:auto, hover rgba(255,255,255,.2).
  • .sc-progressposition:absolute; right:1.25rem; top:50%; translateY(-50%); width:2px; height:120px; background:rgba(255,255,255,.15); overflow:hidden; z-index:10; __bar fills it with a white→blue→pink gradient, transform-origin:top; transform:scaleY(0).
  • @media (max-width:1000px): tags stack vertically at .62rem.

Effect (be exact)

Build in mount(config)destroy().

const DEFAULTS = { scrollSensitivity: 1200, smoothness: .05, imageShift: 25, copyShift: 15, titleHold: .1, imageZoom: 1.25, revealOverlap: .5, bufferSlides: 3 };

State: position = 1, target = 1, columns = { left: { el, live: Map }, right: { el, live: Map } }.

  • Input: wheel (passive) target += deltaY / scrollSensitivity; touch target += (lastY − y) * 8 / scrollSensitivity.
  • create(side, index): n = ((index % 5) + 5) % 5; build the slide with img src = ${side}_${n+1}.jpg, z-index = index + 1000 (later slides stack on top), append, store { el, img, copy } in live.
  • clip(side, reveal): d = clamp(reveal, 0, 1) * (100 + revealOverlap); left → polygon(0% ${100−d}%, 100% ${100−d}%, 100% 100%, 0% 100%); right → polygon(0% 0%, 100% 0%, 100% ${d}%, 0% ${d}%).
  • titlePos(progress): from = progress − 1; past = |from| − titleHold; if past ≤ 0 return 1; t = past / (1 − titleHold); return 1 + sign(from) * t*t*(3 − 2t) (smoothstep away from the plateau).
  • update(): first = floor(position) − bufferSlides, last = floor(position) + bufferSlides + 1; for each side (dir = left ? 1 : −1): create missing indices in [first, last], remove the rest; for each live slide reveal = position − index, progress = clamp(reveal, 0, 2); set clipPath = clip(side, reveal); img.transform = translateY(${(1 − progress) * imageShift * dir}%) scale(${imageZoom}); copy.transform = translateY(${(1 − titlePos(progress)) * copyShift * dir}%). Finally bar.transform = scaleY(frac) with frac = ((position mod 5) + 5) mod 5 / 5.
  • Loop (rAF): position += (target − position) * smoothness; update().

destroy() — cancel rAF, remove listeners, remove all live slides, reset the bar.

Assets / images

Ten large photos (≥1200px tall — each fills half the screen at full height), five matched pairs with a shared mood per project (e.g. mountains/cliffs, sea/ice, road/dunes, milky way/aurora, two buildings). Files left_{1–5}.jpg, right_{1–5}.jpg.

Behavior notes

  • The section captures the wheel for its own virtual scroll; use it as a standalone screen, not inside a normally scrolling page.
  • Because the copy block is 100vw and clipped by each 50vw column, the title is literally cut in half and the two halves move with different slides during a transition — that is the signature.
  • Touch works (drag = scroll); there is no pagination.
  • Only the prefixed variables are global.

Images

This component ships with 10 reference assets, served publicly. Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the same aspect ratios.

https://motionprompts.dev/c/split-column-infinite-slider/left_1.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_2.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_3.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_4.jpg
https://motionprompts.dev/c/split-column-infinite-slider/left_5.jpg
https://motionprompts.dev/c/split-column-infinite-slider/right_1.jpg
… 4 more under https://motionprompts.dev/c/split-column-infinite-slider/

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for demonstration of this component, not for redistribution.

Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

  • Palette on :root--sc-bg, --sc-ink, --sc-line, --sc-shade. These names are not namespaced and they collide: --ink is defined by 164 of the 219 components in this catalogue, --paper by 94, --muted by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (.my-section { --ink: … }) or rename them with a prefix.
  • Rules on html, body — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.