Scroll image sequence
The product film that plays under the wheel.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThree things belong to the page rather than to the component: the section takes no height of its own, its height being one screen plus the scroll the sequence takes; no parent of the section may cut its overflow withoverflow: hidden, which stops the block from pinning, so useoverflow-x: clipwhere a parent has to cut; and the frames need an address that keeps their file names when they are given as a pattern, which the Assets panel of Webflow does not. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Tune it
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the section on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-scroll-sequence Marks the section the page scrolls through while the picture plays. It takes no value: leave the value field empty. That section is what the stylesheet makes taller by the scroll the sequence takes, and what carries the settings. Without it on any element of the page, the embed marks the block holding the screen. data-scroll-sequence-pin Marks the block that stays on screen, a child of the section: the canvas goes behind what it holds. It takes no value. Without it the engine takes the first block of the section. [ Optional ] data-scroll-sequence-frames Marks the box whose pictures are the frames, in the order of the page: swap them, add some, take some out. It takes no value and is never shown. Not needed when the section gives an address and a count with data-scroll-sequence-src and data-scroll-sequence-count; without either, there is no frame and the console says so. [ Optional ] data-scroll-sequence-beat="0.36 0.62" Marks a text that comes and goes: the share of the sequence where it comes, then the share where it leaves, 0 the first frame and 1 the last. A second number of 1 keeps it to the end. [ Optional ] data-scroll-sequence-loader Marks what shows while the frames come in: it is hidden once they are all in. It takes no value. [ Optional ] data-scroll-sequence-bar Marks a rule the load fills: it is scaled across by the share of frames that have arrived. It takes no value. [ Optional ] data-scroll-sequence-loaded Marks a text that receives the share of frames that have arrived, 37% for instance. It takes no value. [ Optional ] data-scroll-sequence-frame Marks a text that receives the number of the frame on screen. It takes no value. [ Optional ] data-scroll-sequence-total Marks a text that receives how many frames the sequence has. It takes no value. [ Optional ] data-scroll-sequence-track Marks a rule the scroll fills: it is scaled across by the place in the sequence. It takes no value. [ Optional ] data-scroll-sequence-alt="A ream of paper opening" On the section: the name assistive technology reads for the picture. Without it the canvas takes the alt of the first picture of the frames box, and is skipped as a decoration when there is none. [ Optional ] data-scroll-sequence-state Written by the engine on the section: idle until the section is near the window, loading while the frames come in, ready once they are all in, still under reduced motion. Never written by hand; it is what the stylesheet reads to pin the block and to hide the loader. [ Optional ] data-scroll-sequence-beat-state Written by the engine on each beat: before, on or after. Never written by hand; the stylesheet reads it to bring the beat in and out. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same section. An attribute always wins over the defaults written in the code.Attribute Default What it is data-scroll-sequence-background #0e0b09 Colour of the block behind the frames: what shows before the first frame arrives, and around the picture when fit is contain. Options: any CSS colour. Give it the colour the frames have at their edges and the picture has no seam. data-scroll-sequence-src empty Address of the first frame, with its number in braces: https://your.host/shot-{001}.webp. The number in the braces is where the count starts and how many digits it is written with. Options: left empty, the frames are the pictures of the box marked data-scroll-sequence-frames, in the order of the page. {001} counts 001, 002, 003; {0} counts 0, 1, 2; {0100} starts at 0100. data-scroll-sequence-count 0 How many frames the address names. Only read with src. data-scroll-sequence-length clamp(1600px, 420vh, 5200px) Scroll the sequence takes, any CSS length: how long the block holds the screen. data-scroll-sequence-smooth 0.16 Time the picture takes to catch the scroll, in seconds. data-scroll-sequence-fit cover How a frame fills the screen: cover or contain. Options: cover fills the block and crops what does not fit, contain shows the whole frame and leaves background around it. data-scroll-sequence-focus-x 0.5 Point of the frame kept in view across, 0 its left edge and 1 its right, when cover crops the sides. data-scroll-sequence-focus-y 0.5 Point of the frame kept in view down, 0 its top and 1 its foot, when cover crops the top and the foot. data-scroll-sequence-blend on Cross-fade between two neighbouring frames while the picture moves: on or off. Options: on draws the two frames around the place in the scroll, one over the other, so few frames still make one move; off shows one frame at a time, for a sequence with hard cuts or fine line work that ghosts. data-scroll-sequence-density 2 Most canvas pixels per CSS pixel. The bitmap follows the screen up to this. data-scroll-sequence-poster 0 Frame shown under reduced motion, as a share of the sequence, 0 the first and 1 the last.
Two files and their pictures (96), no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
ScrollSequence.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-scroll-sequence-background #0e0b09 Colour of the block behind the frames: what shows before the first frame arrives, and around the picture when fit is contain. Options: any CSS colour. Give it the colour the frames have at their edges and the picture has no seam. data-scroll-sequence-src empty Address of the first frame, with its number in braces: https://your.host/shot-{001}.webp. The number in the braces is where the count starts and how many digits it is written with. Options: left empty, the frames are the pictures of the box marked data-scroll-sequence-frames, in the order of the page. {001} counts 001, 002, 003; {0} counts 0, 1, 2; {0100} starts at 0100. data-scroll-sequence-count 0 How many frames the address names. Only read with src. A short sequence, light to load; under 40 frames the cross-fade starts to show as a ghost on a fast move A finer move and more to download: weigh the whole set, 100 to 150 frames is the usual range data-scroll-sequence-length clamp(1600px, 420vh, 5200px) Scroll the sequence takes, any CSS length: how long the block holds the screen. A quick flick through the frames A slow, detailed move, and more wheel to turn before the page goes on. Around 30 to 50 pixels of scroll per frame reads well data-scroll-sequence-smooth 0.16 Time the picture takes to catch the scroll, in seconds. At 0 the picture is locked to the scroll, right with a smooth scroll library already on the page A picture that glides after a wheel notch, and lags behind a fast scroll data-scroll-sequence-fit cover How a frame fills the screen: cover or contain. Options: cover fills the block and crops what does not fit, contain shows the whole frame and leaves background around it. data-scroll-sequence-focus-x 0.5 Point of the frame kept in view across, 0 its left edge and 1 its right, when cover crops the sides. The left of the frame stays, the right is cropped The right of the frame stays. 0.5 keeps the middle, which is what a phone needs for a centred subject data-scroll-sequence-focus-y 0.5 Point of the frame kept in view down, 0 its top and 1 its foot, when cover crops the top and the foot. The top of the frame stays, the foot is cropped The foot of the frame stays data-scroll-sequence-blend on Cross-fade between two neighbouring frames while the picture moves: on or off. Options: on draws the two frames around the place in the scroll, one over the other, so few frames still make one move; off shows one frame at a time, for a sequence with hard cuts or fine line work that ghosts. data-scroll-sequence-density 2 Most canvas pixels per CSS pixel. The bitmap follows the screen up to this. A lighter canvas, softer on a dense screen: 1 is enough for frames that are not larger than the block A sharper picture when the frames have the pixels for it, and more to paint on every frame: past 2 the cost shows before the gain data-scroll-sequence-poster 0 Frame shown under reduced motion, as a share of the sequence, 0 the first and 1 the last. Towards the first frame Towards the last frame. It is the only frame downloaded then -
Drive it from JavaScript
When the frames, the beats or an attribute change after load, or when the page reacts to the load and to the beats.const element = document.querySelector('[data-scroll-sequence]'); element.addEventListener('scrollsequence:change', (e) => { console.log(e.detail); }); const sequence = ScrollSequence.mount(element); // the section auto() mounted, or a new one sequence.progress(); // 0 to 1, the place in the sequence sequence.loaded(); // 0 to 1, the share of frames that have arrived sequence.refresh(); // after changing an attribute, the frames or the beats element.addEventListener('scrollsequence:load', (e) => { console.log(e.detail.loaded, 'of', e.detail.total); }); // When the section leaves the page: ScrollSequence.destroy(element); // removes the canvas and gives back the markup as it was
-
Fit it to your page
- Structure: one section marked
data-scroll-sequence, holding one block markeddata-scroll-sequence-pin: the block that stays on screen. Whatever the block holds, a strip, the texts, a foot, stays with it, over the picture. Without the mark the engine takes the first block of the section. - The frames, the short way: give the section the address of the first frame with its number in braces, and how many there are,
data-scroll-sequence-src="https://your.host/shot-{001}.webp"anddata-scroll-sequence-count="120". The files have to keep their names where they are hosted: a bucket, a CDN, thepublicfolder of a site. - The frames, from the page: with no
src, the frames are the pictures of the box markeddata-scroll-sequence-frames, in the order of the page. This is what the preview does, and the way to keep the frames in the assets of a Webflow site, which renames every file it stores and so breaks a pattern. The box is never shown; its pictures can stayloading="lazy", the engine asks for the files itself. - A beat: any block inside the section with
data-scroll-sequence-beat="0.36 0.62", the share of the sequence where it comes and the share where it leaves. The engine writesdata-scroll-sequence-beat-stateon it,before,onorafter, and the stylesheet does the move: change the rule to change how a beat arrives. A beat whose second number is 1 stays to the end. - The loader and the readouts are optional, and found by attribute anywhere in the section:
data-scroll-sequence-loaderis hidden once the frames are all in;data-scroll-sequence-baris scaled across by the share of frames loaded;data-scroll-sequence-loadedreceives that share as text,37%;data-scroll-sequence-frameanddata-scroll-sequence-totalreceive the frame on screen and how many there are;data-scroll-sequence-trackis scaled across by the place in the scroll. The two shares are also custom properties of the section,--scroll-sequence-loadedand--scroll-sequence-progress, 0 to 1, for any rule of your own. - Making the frames: export a video or a 3D turn as numbered stills, all the same size. WebP at a quality around 75 is the best trade today; the preview's 96 frames of 1600 by 1200 weigh 2.3 MB in all. Keep the subject near the middle, since a phone held upright shows about a third of the width of a 4:3 frame, and give the edges of the frame one flat colour, the one
backgroundtakes. - One setting for every section of the page, without opening the file: declare
window.ScrollSequenceSettings = { smooth: 0 }in a script placed beforescroll-sequence.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-scroll-sequence-*attribute on the section itself (data-scroll-sequence-focus-y="0.3"), so one section can still differ from the page-wide setting. - What the script writes:
data-scroll-sequence-stateon the section,idleuntil the section is near the window,loadingwhile the frames come in,readyonce they are all in,stillunder reduced motion. One canvas, added as the first child of the block, or the page's own<canvas data-scroll-sequence-canvas>when the block has one. The state of each beat, the texts of the readouts, and the two custom properties. - The name of the picture: the canvas takes the
altof the first picture of the frames box, ordata-scroll-sequence-alton the section, as its accessible name. With neither it is hidden from assistive technology, as a decoration. - From JavaScript:
ScrollSequence.mount(element, overrides)returns the instance, withelement,settings,state(),progress()from 0 to 1,loaded()from 0 to 1,frame()andtotal(),refresh()after an attribute, the beats or the frames changed, anddestroy(). The section emitsscrollsequence:changewith{ element, state },scrollsequence:loadwith{ element, loaded, failed, total }for each frame that arrives, andscrollsequence:beatwith{ element, beat, index, state }; the three bubble. - **
destroy()leaves the section as it was**: the canvas removed when the engine added it, the pending downloads dropped, the listeners and the observers gone, the states and the custom properties taken off. - Touch: the sequence follows the page scroll, so a finger drives it as a wheel does. Nothing listens to touch events.
- **
prefers-reduced-motion**: no pin and no sequence. The block is a plain block showing theposterframe, the only one downloaded and held back to 45% so the text over it stays readable, with every beat on the page, one under the other. Turning the preference off brings the sequence back without a reload. - Without the script, the block shows the first picture of the frames box as a cover, held back the same way, and every text.
- Smooth scroll libraries: nothing to wire. The engine listens to scroll events and reads the section's place from the layout, which is what Lenis and the like move. Set
smoothto 0 if the two glides add up. - The words and the pictures of the markup are the preview's, and they ship as they are. Put the user's own in their place whenever they want.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- Structure: one section marked
-
Avoid the pitfalls
- Webflow renames every asset, so a pattern cannot point at them. A file uploaded to the Assets panel gets a random prefix, different for each one. In Webflow, either keep the frames as Images in the frames box and swap them there, or host the files somewhere that keeps their names and give
srcandcount. - An ancestor that cuts its overflow kills the pin, silently.
overflow: hiddenon a parent makes it the scroll container of the sticky block, and a container that does not scroll has nowhere to pin. Useoverflow-x: clipwhere a parent has to cut. The engine says so in the console when it finds one. - Give the section no height of its own. Its height is one screen plus
length, the second added by the stylesheet as the::afterof the section: a fixed height cuts the hold short or leaves a blank, and a section that already uses its::afterloses it while the sequence is on. - Every frame the same size. The crop is worked out for each frame from its own size, so a frame of another shape jumps.
- The whole set is downloaded. Nothing is streamed: 150 frames of 80 KB are 12 MB. Weigh the folder, and bring the size, the quality or the count down before raising
length. The load starts only when the section is within a screen and a half of the window. - A missing frame is skipped, not fatal. The nearest loaded frame stands in, the console names the first address that failed, and the readout ends short of 100%, since it only counts what arrived. It is usually the count or the digits in the braces.
- Frames from another domain are drawn like any picture, with no CORS header needed: the engine never reads the canvas back.
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a section that already carries the effect returns the existing one. A page-wide block goes inwindow.ScrollSequenceSettings, whichauto()reads itself; a later change goes through an attribute andrefresh(). - The beats share one cell only while the sequence runs, one over the other: the stylesheet puts them there once the script has written a state. Before it, and on the canvas of the Webflow Designer, where no script runs, they sit one under the other and each can be read and edited.
- Webflow renames every asset, so a pattern cannot point at them. A file uploaded to the Assets panel gets a random prefix, different for each one. In Webflow, either keep the frames as Images in the frames box and swap them there, or host the files somewhere that keeps their names and give
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Scroll image sequence is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called image sequence on scroll, scroll scrubbed video or canvas image sequence.
The product film that plays under the wheel. A section holds the screen while the page scrolls through it, and its picture is a sequence of frames: the scroll decides which one is shown, forwards and back, eased towards the wheel instead of snapped to it. The frames are drawn on a canvas laid out like object-fit cover, with a focal point to keep in view when a phone crops the sides, and a bitmap that follows the pixel density of the screen. Two neighbouring frames are cross-faded while the picture moves, so a short sequence still reads as one move, and the picture always comes to rest on a whole, sharp frame. The frames load in passes, the one on screen first, then one in sixteen, in eight, in four, so the whole move is there, coarse, long before the last file; a loader says how many are in, counted, not guessed. Texts marked as beats come and go at given points of the sequence. The frames are given either as an address pattern and a count, or as the pictures of a box of the page, which is how a Webflow site keeps them in its own assets. No library: the hold is sticky positioning, the position is read from the layout, so it follows the browser's own scroll and a smooth scroll library alike. Under reduced motion there is no pin and no sequence: one frame, the only one downloaded, and every text on the page. Pattern type image sequence, scroll scrubbed video, product reveal.
Updated
Questions
Mark a section data-scroll-sequence and the block that stays on screen inside it data-scroll-sequence-pin. Keep the frames as Images in a box marked data-scroll-sequence-frames, in the order of the page: Webflow renames every asset it stores, so an address pattern cannot point at them. The box is never shown, and the engine asks for the files itself.
No. The hold is CSS sticky positioning and the place in the sequence is read from the layout, so it follows the browser's own scroll and a smooth scroll library alike, with no library loaded. The picture is numbered stills drawn on one canvas, not a video: the preview plays 96 WebP frames of 1600 by 1200, 2.3 MB in all.
100 to 150 frames is the usual range; under 40 the cross-fade between two frames starts to show as a ghost on a fast move. data-scroll-sequence-length is the scroll the sequence takes, and around 30 to 50 pixels of scroll per frame reads well. The whole set is downloaded, nothing is streamed, so weigh the folder before raising the count.
Give any block inside the section data-scroll-sequence-beat="0.36 0.62", the share of the sequence where it comes and the share where it leaves. The engine writes data-scroll-sequence-beat-state on it, before, on or after, and the stylesheet does the move. A beat whose second number is 1 stays to the end.
An ancestor with overflow: hidden becomes the scroll container of the sticky block, and a container that does not scroll has nowhere to pin. Use overflow-x: clip where a parent has to cut; the engine says so in the console when it finds one. Give the section no height of its own either: its height is one screen plus the length of the sequence.