# Curved Ribbon Slider

> A full-bleed WebGL slider driven by the scroll wheel. A distortion field sits in the middle of the container and bends whatever scrolls through it: the panel at the front stays flat and sharp while the ones either side curl away and smear into a spectrum, mirrored on both sides.

Canonical: https://gsapvault.com/effects/curved-ribbon-slider
Live demo: https://gsapvault.com/demos/curved-ribbon-slider/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | webgl-shader, chromatic-aberration, scroll-hijack, velocity-reactive, infinite-loop, snap, carousel |
| Uses Lenis | No |

## Overview

A full-bleed slider rendered in WebGL and driven by the scroll wheel. The distortion is a field anchored to the container rather than an animation applied to each panel: it sits in the space, and whatever scrolls through it is bent by it. The panel in the middle is always flat and always sharp; toward either edge the surface curls away, tapers to a point and smears into a spectrum, the two sides mirroring each other.

Scroll and the whole ribbon squashes like jelly in the direction of travel, the colour separation throwing wider the faster you go, before it eases to a stop and snaps the nearest panel square. Two-finger trackpad swipes, dragging and arrow keys all drive the same motion.

It is three.js and about a hundred lines of GLSL, not a library of sliders. It loops forever from any number of images, cover-fits whatever aspect you give it, and falls back to a plain scroll-snap rail without JavaScript, without WebGL, or under reduced motion.

## Features

- The distortion is a field fixed to the container, not an animation on each panel, so it stays put and augments whatever passes through it
- Mirrored by construction: the two ends of the ribbon are reflections of each other rather than two copies of the same curl
- The middle of the field is mathematically untouched, so the panel being read is flat and sharp no matter what the rest of the ribbon is doing
- A real spectral smear, not a red and blue offset: twelve taps across the image, each weighted by a different band of the spectrum, concentrated where the surface is turning
- Scroll-velocity jelly, bowing the ribbon in the direction of travel so a fast scroll has visible weight
- Frame-rate independent: the easing and the velocity are both computed against real elapsed time, so it behaves identically at 30, 60 and 120Hz
- Infinite looping from a fixed pool of meshes that are re-textured rather than cloned, so eight images and eighty cost the same
- Renders only while on screen, and disposes every geometry, material, texture and the WebGL context itself on teardown

## Use Cases

- Agency and studio landing pages that need one signature above-the-fold moment
- Photography and print portfolios where the imagery is the entire pitch
- Case-study and project indexes where each entry deserves the whole viewport
- Product and collection showcases built around one hero image at a time
- Launch and award-entry pages that want a interaction people will remember

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

The ribbon is a flat row of textured planes under an orthographic camera, so one world unit is one CSS pixel and there is no perspective at all. Every curve is vertex displacement, and every term in it is a function of where a vertex sits ON SCREEN rather than of which panel it belongs to. That is what makes the distortion a field: it stays in the container and bends whatever scrolls through it, instead of travelling along with a panel and leaving a gap wherever a panel's folding edge has left the viewport.

Past a clean central band the surface bends at constant curvature, so arc length becomes sin(theta) across and 1-cos(theta) perpendicular, and once the turn reaches its ceiling the remainder carries straight on along the tangent. Without that second phase the mapping saturates and panels a full pitch apart pile up on the same pixels. Mirroring is a single sign term, so the two ends are reflections rather than repeats.

The fragment shader samples the image twelve times along a horizontal offset, weighting each tap by a different band of a cosine spectrum. At zero offset the taps collapse back to the original pixel, so the flat middle takes a single sample on a fast branch and stays sharp. The wheel feeds a target position chased with a time-based exponential ease, and frame-to-frame travel is normalised into panel widths per sixty-hertz frame and handed to the shaders as the scroll speed.

## Integration Preview

How this effect integrates into a page. The full documentation (examples, events, programmatic API, customization guide) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

```html
<link rel="stylesheet" href="assets/style.css">
<script>
  (function () {
    try {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      var c = document.createElement('canvas');
      var gl = c.getContext('webgl2') || c.getContext('webgl');
      if (!gl) return;
      var lose = gl.getExtension('WEBGL_lose_context');
      if (lose) lose.loseContext();
      document.documentElement.classList.add('gl');
    } catch (e) {}
  })();
</script>
```

The inline script is not optional. The image list is the fallback for no
JavaScript, no WebGL and reduced motion, and without this it paints as a
plain rail for the half second it takes three.js to arrive, then gets
swapped for the canvas. The probe runs before first paint, stamps `html.gl`
when a canvas is coming, and the stylesheet hides the list under that class.
No JavaScript, no class, rail shows; and the effect removes the class again
if it cannot build a renderer after all.

**2. Add to your `<body>`:**

```html
<div class="ribbon" data-ribbon tabindex="0" role="group" aria-roledescription="carousel" aria-label="Projects">
  <ul class="ribbon__slides" data-ribbon-slides>
    <li data-ribbon-slide data-label="Colonnade">
      <img src="img/one.jpg" alt="A colonnade of concrete columns." draggable="false">
    </li>
    <li data-ribbon-slide data-label="Rigging">
      <img src="img/two.jpg" alt="Masts and rigging against an overcast sky." draggable="false">
    </li>
    <!-- two or more -->
  </ul>
</div>
```

**3. Add before the closing `</body>` tag:**

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.min.js"
  }
}
</script>
<script type="module">
  import * as THREE from 'three';
  window.THREE = THREE;
  const effect = document.createElement('script');
  effect.src = 'assets/script.js';
  document.body.appendChild(effect);
</script>
```

The script reads the list, drops a canvas in front of it, and hides the list
once the canvas is actually there.

**Why the import map rather than a plain `<script src>` for three.js:** three
ships as ES modules only, and its old UMD build logs a deprecation warning on
every page load. The shim above imports it as a module, puts it on `window`,
and then loads `assets/script.js` as an ordinary script, so the effect itself
stays a plain file you can drop into any build (or none).

**Already using three.js as a module?** Then skip the shim, and just make sure
`window.THREE` is set before `assets/script.js` runs:

```javascript
import * as THREE from 'three';
window.THREE = THREE;
```

---

### Options

All set on the `[data-ribbon]` element.

| Attribute | Values | Default | Description |
|---|---|---|---|
| `data-panel-width` | number | `0.34` | Panel width as a fraction of the container width. The framing dial: it decides how many panels are on screen and therefore how many are inside the distortion field. |
| `data-aspect` | number | `1.5` | Panel aspect ratio. Images are cover-fitted into it. |
| `data-max-panel-height` | number | `0.72` | Ceiling on panel height as a fraction of the container height, for tall narrow windows. |
| `data-gap` | number | `0.035` | Gap between panels, as a fraction of the container width. |

**The field.** These four describe the distortion itself. It is anchored to the
container, not to the panels: it sits in the space and bends whatever slides
through it, and the two sides mirror each other automatically.

| Attribute | Values | Default | Description |
|---|---|---|---|
| `data-flat-band` | number | `0.42` | Half-width of the undistorted band in the middle, as a fraction of half the container width. Everything inside it is untouched, which is what keeps the panel being read perfectly flat and sharp. |
| `data-curvature` | number | `2.1` | How tightly the surface bends past the band, in radians turned over one half-container-width. Higher is a tighter curl. |
| `data-max-angle` | radians | `1.45` | Ceiling on that turn. **Keep it below `1.57` (90°)** unless you want the surface folding back over itself: past a right angle the tail travels inward again and overlaps whatever is behind it. |
| `data-pinch` | number | `0.62` | How hard the curled part tapers to a point. This is what makes it a peel rather than a bent ribbon of constant width. |
| `data-curl-dir` | number | `-0.85` | Which way the curl sweeps and how far. Negative drops the left end and lifts the right; positive swaps them. The two ends always rotate in opposite senses, so the ribbon reads as one continuous surface passing through the flat band rather than as a valley with a curl hanging off each side. |
| `data-tail-fade` | number | `0.62` | How far the ends run past the end of their turn before they have dissolved completely, as a fraction of the climb it would take them to leave the top of the container. Past the turn the surface travels almost straight up and down, so without this the ends streak off into the corners. Lower it to keep the framing tight on the panels that are still turning; raise it past `1` to let them run off the edge instead of fading. |

**Everything else.**

| Attribute | Values | Default | Description |
|---|---|---|---|
| `data-wave` | number | `0.05` | A small wobble through the bend, so it reads as paper rather than pressed tin. |
| `data-dispersion` | number | `0.22` | Spectral separation at full bend, in UV units. |
| `data-blur` | number | `0.05` | How far out of focus the surface goes as it turns away, in UV units. Rides the same curve as the dispersion, so the panel being read stays sharp however high this goes. |
| `data-glow` | number | `0.85` | How hard the neon bloom sits on top of the fringe. `0` turns it off. |
| `data-speed-dispersion` | number | `0.9` | Extra separation earned by scrolling, applied across everything on screen. |
| `data-jelly` | number | `1.25` | Scroll-velocity squash. |
| `data-ease` | number | `0.085` | Fraction of the remaining distance covered per 60fps frame. Lower is heavier. |
| `data-wheel` | `true` / `false` | `true` | Whether to take over the wheel. See below. |
| `data-snap` | `true` / `false` | `true` | Whether to settle square onto a panel. |

#### About `data-wheel`

By default the ribbon **calls `preventDefault()` on wheel events over it**, so
scrolling with the pointer above it moves the ribbon instead of the page. That
is right for a full-viewport hero and wrong almost everywhere else: a visitor
who cannot scroll past your slider will leave. Set `data-wheel="false"` for a
ribbon sitting mid-page, and it becomes drag, keyboard and button driven while
the page scrolls normally over the top.

#### Example: a mid-page ribbon, calmer, showing more of its neighbours

```html
<div class="ribbon" data-ribbon
     data-wheel="false"
     data-panel-width="0.3"
     data-flat-band="0.55"
     data-curvature="1.4"
     data-pinch="0.35"
     data-dispersion="0.1"
     data-jelly="0.7">
  ...
</div>
```

#### Example: a tighter curl over a narrower clean band

```html
<div class="ribbon" data-ribbon data-flat-band="0.3" data-curvature="3.2" data-curl-dir="0.9">
  ...
</div>
```

---

### Accessibility

- **Keyboard**: the ribbon is focusable; left and right arrows step it. This is
  the only non-pointer route in, so keep it: the demo ships no visible
  next/previous buttons, by design.
- **Screen readers**: the canvas is not announced. Announce the current panel
  yourself from a live region fed by `ribbonchange` — the demo page shows the
  pattern.
- **A note on the field**: because the distortion is anchored to the container
  rather than to the panels, the panel in the middle is always undistorted and
  always legible. That is a deliberate accessibility property, not a
  coincidence of the look: widening `data-flat-band` widens the readable zone.
- **Reduced motion**: the WebGL ribbon never starts. The source list stays
  exactly as it is, a plain horizontal scroll-snap rail of the same images.
- **Without JavaScript, or without WebGL**: the same rail, for the same reason.
  One fallback, three failure modes, and it is never hidden until a canvas has
  actually been created.
- **The wheel takeover is a real accessibility decision.** Left on, a visitor
  scrolling down the page stops dead over the ribbon. It is defensible for a
  full-viewport hero that *is* the page at that point, and hard to defend
  anywhere else. See `data-wheel`.

---

### Dependencies

| Dependency | Version | Required |
|---|---|---|
| three.js | 0.180.0 | Yes, as an ES module (see Quick Start) |
| GSAP core | 3.14.2 | Yes — matchMedia branching and teardown only |

No GSAP plugins. The animation runs on the renderer's own `requestAnimationFrame`
loop rather than a GSAP tween, because it is driven continuously by scroll
rather than played.

Everything the effect uses (`WebGLRenderer`, `OrthographicCamera`,
`PlaneGeometry`, `ShaderMaterial`, `TextureLoader`) is long-stable three.js
API, so pinning to a different version is a one-line change in the import map.

---

### Browser Support

Anything with WebGL, which is every current browser. Support is **probed**
before three.js is asked for a renderer, deliberately: three logs its own
failure to the console, as errors, several times over, before it throws, so
catching the exception would hide nothing and a visitor with WebGL disabled
would get a console full of red on a page that had quietly fallen back. Probed
first, that visitor simply gets the scroll-snap rail and a clean console.

Import maps are supported everywhere current. In a browser old enough to lack
them the module never runs, `window.THREE` is never set, and the rail is again
what shows.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Curved Ribbon Slider](https://gsapvault.com/effects/curved-ribbon-slider)
- [Effects & Templates Vault (£39 one-time, best value): every current and future effect and template](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Image Clip Reveal](https://gsapvault.com/effects/image-clip-reveal.md): A cinematic image reveal where a directional polygon aperture opens as the photograph settles from a restrained Ken Burns scale and its editorial caption lands.
- [3D Card Flip Gallery](https://gsapvault.com/effects/3d-card-flip.md): Tactile 3D cards with deep perspective, reactive edge-light, shadow inversion, hover/focus parity, and tap auto-close.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
