Scroll✨ Premium

Image Sequence

A single div whose 24 states (gradient, corner radius, rotation, size from 60 to 118 px) live in a JS array and switch according to the position of a 280 px scroll container with 920 px of travel — one step every 38 px, no image, 1.9 KB of CSS and 3.5 KB of JS. The first 12 steps are nearly invisible on the background.

JavaScriptCSSSequence

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →

3 usage examples

Fitting the sensor module — built-in help of a connected device — Image Sequence example 1

① Fitting the sensor module — built-in help of a connected device

WhenHelp screen of an IoT box with a minimal browser (no WebGL, a few KB of storage): four steps A → D, and a shape that transforms as you scroll.
WhyNo image, no WebGL: the 24 states are an array, the whole thing weighs 5.4 KB of CSS and JS, nothing is downloaded or stored — whereas fx-0795 (Pinned Image Sequence) loads a .glb into <model-viewer> and requires WebGL. Step A/B/C/D is derived from the same position (Math.floor(r × 4)) to highlight the current instruction.
SettingsCode as sold (ids, 24 states, 280 px container, 920 px travel); the four instructions read the same scrollTop; translated hint; counter and hint raised to 55% white (5.4:1); transition and pulse switched off under prefers-reduced-motion.
Three finishes to scroll through — configurator of a desk — Image Sequence example 2

② Three finishes to scroll through — configurator of a desk

WhenFurniture product page: the top changes finish — natural oak, anthracite grey, matte white — and its name is displayed; price and button stay put.
WhyEach state is a named entry of the array, with a readable index: the finish name, the active chip and, in real life, the SKU or the price follow from it. fx-0585 (Sticky Image Change) also switches four gradients on scroll, but through @keyframes on animation-timeline: scroll(): no index, no name, and nothing in Firefox.
SettingsThree states {bg, shape: 14px, rot: 0, size: 150} with a name field; .imgseq-inner at 640 px for a 320 px container (107 px per finish); counter replaced by the name in role="status"; ids suffixed -2.
k-means in three steps — figure of a data-science article — Image Sequence example 3

③ k-means in three steps — figure of a data-science article

WhenTechnical article in static Markdown (GitHub Pages): on the right, a point cloud goes from initialisation to assignment then convergence while the reader scrolls.
WhyThe “film” fits in three background strings: stacked radial-gradients draw 12 points and 3 centroids, recoloured then regrouped — a JS array, no image, no library, pasted into the page without a build step. fx-0795 would have needed a WebGL runtime and a model to host.
SettingsThree states, size: 170, shape: 12px, rot: 0, bg = a list of 15 radial-gradients per state; 300 px container, .imgseq-inner 720 px; figure caption bound to the index.

How it works

The .scroll-container (100% × 280px, overflow: hidden auto, scroll-behavior: smooth, a 4 px scrollbar styled through ::-webkit-scrollbar only) holds .imgseq-inner, an empty 1200px block: the travel is therefore 1200 − 280 = 920 px. Inside, .imgseq-display is position: sticky; top: 0 with a height of 280px, the container's own — the stage fills the frame from start to end. It carries the “film”, .imgseq-frame, and the counter .imgseq-counter (“Frame 1 / 24”, 10.4 px, white at 30%). The sold HTML already sets state 1 on the frame as a style attribute, exactly as the script had left it (gradient #1a1a2e → #16213e, 20 px radius, 60 × 60); the script rewrites it on load, which is harmless — but the CSS's 120px never applies, nor do font-size: 1.5rem and font-weight: 800 on a box with no text. The ::before is a 10% white ring whose radius stays at 20 px whatever the state, and .scroll-hint-label (“Scrollez pour explorer”) pulses from .3 to .7 opacity every 2 s.

The frameStates array holds 24 objects {bg, shape, rot, size}. On every scroll event of the container, the script computes r = scrollTop / (scrollHeight − clientHeight), then i = min(floor(r × 24), 23), and writes five inline styles: background, border-radius, transform: rotate(), width, height, plus the counter text. Measured: the steps fall at 0, 39, 77, 115 … 882 px — 38.33 px each, the 24th spanning 882 to 920. The frame's transition: .2s eases size, radius and rotation (60 → 118 px in 240 ms on a jump from 0 to 700 px) but not the gradient, which browsers don't interpolate: it is already replaced 61 ms after the jump. With a notched mouse (100 px per notch in Chrome), one notch is worth 2.6 steps: measured, the displayed sequence is 1, 3, 6, 8, 11, 14, 16, 19, 21, 24 — 10 states out of 24; only a trackpad or a free-spinning wheel goes through all of them.

The states themselves: twelve dark gradients first (from midnight blue #1a1a2e → #16213e to plum #6a1a3c → #6e2168), whose contrast against the #0a0a0f background measures 1.20 to 1.83:1 — for the first 460 pixels, half the travel, you see a disc barely lighter than black grow from 60 to 95 px and turn from 0 to 165°; at state 13 the gradient jumps to indigo #6366f1 → #7c3aed (3.84:1), then violet, fuchsia, pink, orange, amber, lime, cyan (12.24:1 at state 22) and back to indigo. The radius goes from 20 to 10 px (states 1–7), becomes a circle (50%, 8–14), comes down from 45 to 25% then from 22 to 14 px; the rotation advances 15° per step up to 345°. It is a nested scroller: measured with 2000 px of page below the zone, the first ten notches (920 px) are absorbed by the container before the page resumes, and as many on the way back up. The scroll listener is attached to the container only and never removed — it goes away with the element; no requestAnimationFrame, no resize listener (the travel is re-read on every event). No console error, no site CSS variable.

Accessibility

  • prefers-reduced-motion not handled: measured under reduce, the hint still pulses (pulseHint 2 s infinite), the .2 s transition stays active and the states still switch — including a 270° spin in 200 ms when a wheel jump crosses 18 steps. The scroll-driven change is user-initiated (tolerated by WCAG 2.3.3); the pulsing and the spin are not: add @media (prefers-reduced-motion: reduce) { .imgseq-frame { transition: none } .scroll-hint-label { animation: none } } and set rot to 0 in the array.
  • Keyboard: the zone is a div without tabindex. Chromium nevertheless makes scroll containers focusable — measured: Tab lands on .scroll-container, each Arrow Down is worth 40 px, two presses show state 3; Firefox does the same, Safari doesn't. Set tabindex="0" and an aria-label (or role="region") so it holds everywhere; Page Down covers 245 px, six steps at once.
  • Screen readers: the counter is rewritten on every scroll event without aria-live, the frame is an empty div and the ring a ::before — nothing is announced, nothing is named. Put aria-hidden="true" on .imgseq-frame and on the hint; if a state carries meaning (a finish name, a step number), announce it in a role="status" region updated only when the index changes — the script itself writes its five styles and the text even when the index hasn't moved.
  • Contrast and text: counter and hint in rgba(255,255,255,.3) on #0a0a0f measure 2.62:1 at 10.4 px, below 4.5:1 — raise to .5 (5.36:1) or .6 (7.30:1). The counter says “Frame” (a JS string), the hint “Scrollez pour explorer” (HTML): two languages in the sold code. Finally, the zone captures 920 px of wheel before handing the page back — ten notches — with no signal other than the hint; shorten .imgseq-inner or keep the zone away from the main reading flow.

Browser compatibility

ES2015 JavaScript (const, arrow function, template string). CSS: position: sticky, inset, two-value overflow, font-variant-numeric, scroll-behavior. The switching relies on a plain scroll listener: it works in Firefox, unlike the effects of this category written with animation-timeline: scroll() (fx-0585, fx-0545…), which no Firefox version enables. Zero dependencies.

Chrome 87+✓ Full
Firefox 66+✓ Full (native, unstyled scrollbar)
Safari 14.1+✓ Full
Edge 88+✓ Full
Mobile iOS 14.5+✓ Full (touch scrolling)
Android Chrome✓ Full (touch scrolling)

Without JavaScript, the zone scrolls its 920 px for nothing: the frame stays at state 1 written in the attribute (60 px, midnight blue) and the counter reads “Frame 1 / 24” (measured). Without inset (pre-2020 browsers), only the 10% white ring disappears. Without position: sticky, the stage scrolls away with the 1200 px block and leaves the frame.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="scroll-container" id="imgseq-scroll">
  <div class="imgseq-inner">
    <div class="imgseq-display">
      <div class="imgseq-frame" id="imgseq-frame"></div>
      <div class="imgseq-counter" id="imgseq-counter">Frame 1 / 24</div>
    </div>
  </div>
</div>
<span class="scroll-hint-label">Scrollez pour explorer</span>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
State array (JS — frameStates) 24 objects {bg, shape, rot, size} Add or remove entries: the count follows (totalFrames = frameStates.length) and the steps are recomputed (travel ÷ n). bg accepts any background value: a gradient list, a solid colour, the url() of a preloaded image.
Travel (CSS — .imgseq-inner { height: 1200px } and .scroll-container { height: 280px }) 1200 − 280 = 920 px, i.e. 38 px per step The difference between the two heights is the distance to cover. At a 600 px block, the travel drops to 320 px and one wheel notch (100 px) skips seven steps; aim for at least 100 px per state if a notched mouse must show them all.
Rotation (JS — rot) 0 → 345° in 15° steps Written as transform: rotate() and eased by the transition: a jump of several steps turns the shape by as much in 200 ms. Set 0 everywhere for a product photo or a figure.
Size (JS — size) 60 → 118 (state 19) → 95 px Applied to both width and height: the frame is always square. For a rectangle, add a field and write the two dimensions separately.
Transition (CSS — .imgseq-frame { transition: .2s }) .2s on all properties Eases size, radius and rotation; the gradient always snaps (measured: replaced within 61 ms). 0s for a film with hard steps, .4s for a slower morph, none under prefers-reduced-motion.
Palette (JS — bg) 12 dark gradients (#1a1a2e#6a1a3c) then 12 vivid ones (#6366f1#22d3ee) The dark half measures 1.20 to 1.83:1 on #0a0a0f, barely visible. Start the array at state 13, or lighten the first twelve; on a light background it is the reverse: the lime and amber states weaken.
Counter (JS — `Frame ${i+1} / ${totalFrames}`; CSS — .imgseq-counter) “Frame n / 24”, 10.4 px, white at 30% Rewritten on every event. Replace the template with a field of the state (t.name) to show a name; raise the opacity to .5 (5.36:1) to clear the 4.5:1 threshold.
Hint (HTML — .scroll-hint-label; CSS — pulseHint) “Scrollez pour explorer”, 2 s infinite pulse French text hard-coded in the HTML, to translate or remove; the animation never stops and is not gated on prefers-reduced-motion.

FAQ

Nowhere. The 24 “images” are CSS states — a gradient, a radius, a rotation, a size — stored in frameStates and applied to a single div; nothing is loaded. For real photos, give each state bg: "url(frame-01.jpg) center / cover", keep size constant and rot: 0, and preload the files (new Image().src = … on load): the swap happens at scroll time, an image that hasn't arrived leaves the frame empty, and the .2 s transition interpolates two images no more than it does two gradients. The version with a real 3D object is fx-0795 (Pinned Image Sequence): a .glb model in <model-viewer>, hence WebGL and a file to host. This one costs 5.4 KB and runs on any browser that executes JavaScript.
fx-0585 is pure CSS: a four-stop @keyframes (0, 25, 50, 75%) on animation-timeline: scroll(), no JavaScript — but also nothing to read: no index, no name, no event, and no Firefox version enables animation-timeline, so there the block stays on its first gradient. fx-0583 goes through a scroll listener: it works in Firefox 66+, and it exposes i and the state object on every event — enough to display a name, sync a caption or trigger a load. The price: six style writes per event instead of a composited animation. Pick fx-0585 for a backdrop nothing else needs to follow, fx-0583 as soon as a state has a name or other elements must follow it.
Because states 1 to 12 are near-black gradients: measured at the middle of the gradient, 1.20:1 (state 1) to 1.83:1 (state 12) against the #0a0a0f background, below the 3:1 expected of a graphical element; the ::before ring at 10% white is 1.05:1 on state 1. During these first 460 pixels — exactly half of the 920 px travel — only the size (60 → 95 px) and the rotation (0 → 165°) change, on a shape you can hardly make out. At state 13 (from 460 px), the gradient jumps to #6366f1 → #7c3aed (3.84:1) and the palette stays vivid to the end. Two remedies: start the array at state 13 (twelve 77 px steps, easier to read with a wheel), or raise the lightness of the first twelve gradients.