061

Curved Ribbon Slider

£10 or the whole Vault, £99

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.

advanced
7 more details
webgl-shaderchromatic-aberrationscroll-hijackvelocity-reactiveinfinite-loopsnapcarousel

This demo reads better at your own screen size than in the frame below:

Released

About this effect

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.

Read the full effect overview

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.

Curved Ribbon Slider - GSAP animation effect preview

What's included

8 items
  • 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

Perfect for

5 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

How it works

3 sections

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.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Curved Ribbon Slider opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse free effects

Or the whole library: the Vault, £99, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

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>:

Code snippet omitted: it ships with the download.

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

Code snippet omitted: it ships with the download.

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:

Code snippet omitted: it ships with the download.


Choosing Your Images

This effect has a real content requirement, and ignoring it is the main way to make it look broken. The dispersion drags the colour channels apart horizontally. So:

  • Vertical structure separates into colour. Horizontal structure does not. Columns, trunks, masts, doorways, railings, standing figures, window mullions: these split into a visible spectrum. A horizon, floorboards, a stack of shelves: these barely register and the panel reads as a grey smear.
  • Mid to high contrast. The spectrum only appears where neighbouring pixels differ. Flat, hazy, low-contrast images come out muddy.
  • Keep the subject centred. Off-centre panels are compressed to about two thirds of their width, so anything near the left or right border is the first thing the curve eats.

Test at speed, not at rest. Scroll the ribbon hard and look at each panel: any image that greys out instead of colouring is the wrong image for this effect.

Images do not need to share an aspect ratio. The fragment shader cover-fits each one into the panel, so a portrait frame and a panorama both fill it without distortion.


Using It With Your Own Design

What the effect actually requires of your markup: a container with data-ribbon, and inside it a list with data-ribbon-slides whose items carry data-ribbon-slide. Each item needs an image, either as a real <img> or as a data-image attribute holding a URL. Everything else is yours.

The container must have a real size. The canvas is sized from the container's clientWidth/clientHeight, so .ribbon needs a height from somewhere: it is position: absolute; inset: 0 in the demo. Give it a height of 0 and you get a canvas of 0.

Non-obvious things the effect depends on (keep these if you restyle):

  • touch-action: pan-y on .ribbon. On touch there is no wheel to take over, so this is what keeps vertical page scrolling working from on top of it.
  • The .ribbon__slides list must stay in the document. It is the fallback for no JavaScript, no WebGL and reduced motion, and it is only hidden by html.gl (the head probe, before first paint) and .ribbon.is-live (the script, once a canvas exists).
  • The ribbon's own background shows through wherever a panel does not cover it, so it wants to be dark for the dispersion to read.

What is only the demo's CSS and can be deleted: .ribbon__chrome, .ribbon__count, .ribbon__label, .ribbon__steps, .ribbon__step, and the whole .showcase-* shell. The counter and label are wired up by the page, not by the effect — see Events.


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

Code snippet omitted: it ships with the download.

Example: a tighter curl over a narrower clean band

Code snippet omitted: it ships with the download.


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.15.0 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.

Performance

Five or so meshes and one draw call each. The expensive part is the nine-tap dispersion, and the front panel skips it on a branch, so the sharp panel you are actually reading is also the cheapest one on screen. The loop is gated on an IntersectionObserver and does not run at all while the ribbon is off screen.

To buy back frames on low-end hardware: lower data-speed-dispersion first (the taps are the cost, and this is the term that applies them to the whole panel rather than just the fold), then data-panel-width so fewer panels are on screen.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.