Velocity marquee
A ribbon of content runs on without end, and the page scroll drives it: it cruises on its own when nothing is happening, gains speed as the reader scrolls down, runs back the other way when the reader scrolls up, and leans a little while it is being pulled, standing upright again the moment it settles.
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 tutorialThe space between two items belongs to the items, as a padding or a margin on each of them, so the rhythm holds where two copies join. A row that spaces its items with a flex gap has no space at that join: give--velocity-marquee-gapthe same length, or move the space onto the items. -
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 -
Use it on your own elements
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 element 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-velocity-marquee Marks the box the ribbon runs behind. It takes no value: leave the value field empty. That box is what clips the content, what the effect is mounted on and what carries the settings. Without it nothing is mounted anywhere, and the embed says so in the console. data-velocity-marquee-track Marks the row that holds the content, inside that box, written once. It takes no value, and it decides what the ribbon is made of: the engine repeats this row as many times as the width needs. Without it the engine wraps everything inside the box in a row of its own and the ribbon still runs, but the whole content of the box becomes the ribbon, padding and headings included. [ Optional ] data-velocity-marquee-strip Written by the engine on the box it builds around the row, and taken off again by destroy(). Never written by hand: it is in this table because it shows in the Navigator and because the stylesheet styles it. [ Optional ] data-velocity-marquee-clone Written by the engine on every copy of the row. A copy is hidden from screen readers and taken out of the tab order, and it is made again whenever the width or the content changes. Never written by hand. [ Optional ] data-velocity-marquee-state Written by the engine on the box: running, paused while it is off screen, still under reduced motion or when there is nothing to repeat. Never written by hand, and a page can style on it. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-velocity-marquee-cruise 46 How fast the ribbon runs on its own, in pixels a second, when the page is not moving. data-velocity-marquee-direction left Which way the ribbon runs when nothing is happening. Options: left, right. data-velocity-marquee-boost 1.5 How much of the scroll speed is handed to the ribbon, as a factor. data-velocity-marquee-max-speed 2400 The ceiling, in pixels a second, whatever the scroll does. data-velocity-marquee-ease 0.12 How quickly the ribbon takes up a new speed, 0 to 1. data-velocity-marquee-skew 7 The strongest lean, in degrees, reached at top speed. data-velocity-marquee-reverse true Whether scrolling up sends the ribbon back the other way. Options: false (the page only ever pushes it forward), true. data-velocity-marquee-reduced-speed 0 The speed kept when the reader asks for reduced motion, in pixels a second. Options: 0 stands the ribbon still and makes no copy, any speed keeps it crawling with no lean. data-velocity-marquee-debug false Whether the ribbon writes its live state in data-velocity-marquee-debug-* attributes. Options: false, true (speed, lean, period, copies and offset, six times a second).
Two files, no build step. It also needs GSAP 3 on the page, loaded with the tag in step 1.
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
VelocityMarquee.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-velocity-marquee-cruise 46 How fast the ribbon runs on its own, in pixels a second, when the page is not moving. Nearly still between two scrolls Runs on briskly with no help data-velocity-marquee-direction left Which way the ribbon runs when nothing is happening. Options: left, right. data-velocity-marquee-boost 1.5 How much of the scroll speed is handed to the ribbon, as a factor. The scroll barely shows in the ribbon A flick of the wheel throws the ribbon across data-velocity-marquee-max-speed 2400 The ceiling, in pixels a second, whatever the scroll does. A calm ribbon that refuses to race A ribbon that can turn into a blur data-velocity-marquee-ease 0.12 How quickly the ribbon takes up a new speed, 0 to 1. Long, heavy, keeps running after the scroll stops Snappy, glued to the scroll, close to nervous data-velocity-marquee-skew 7 The strongest lean, in degrees, reached at top speed. Upright at any speed A pronounced slant, watch the legibility data-velocity-marquee-reverse true Whether scrolling up sends the ribbon back the other way. Options: false (the page only ever pushes it forward), true. data-velocity-marquee-reduced-speed 0 The speed kept when the reader asks for reduced motion, in pixels a second. Options: 0 stands the ribbon still and makes no copy, any speed keeps it crawling with no lean. data-velocity-marquee-debug false Whether the ribbon writes its live state in data-velocity-marquee-debug-* attributes. Options: false, true (speed, lean, period, copies and offset, six times a second). -
Drive it from JavaScript
When the ribbon has to stop and start on its own, when the content changes after load, or when the page reacts to its speed or to its turns.const element = document.querySelector('[data-velocity-marquee]'); element.addEventListener('velocitymarquee:ready', (e) => { console.log(e.detail); }); const ribbon = VelocityMarquee.mount(element); // the box auto() mounted, or a new one ribbon.pause(); // out of the frame loop, and ribbon.play() back into it ribbon.speed(); // px/s now; ribbon.lean() the degrees, ribbon.period() the wrap distance ribbon.measure(); // read the period and the copies again, after a layout change of your own // Content added after the page loaded: mount everything marked inside it. VelocityMarquee.auto(document.body); // When the ribbon leaves the page: VelocityMarquee.destroy(element); // removes the strip and the copies, hands the row back
-
Fit it to your page
- Mark the box, not the section:
data-velocity-marqueegoes on the ribbon itself, the box with the height, the rules and the type. A page can hold as many as it likes and they stay independent, each with its own speed and its own offset. - Write the content once, in the row marked
data-velocity-marquee-track. Anything goes inside: words, links, images, badges, an icon between two items. The engine copies that row as many times as the width needs, and counts again when the width, the font or the content changes. - Settings go on the box, as more attributes:
data-velocity-marquee-direction="right",data-velocity-marquee-skew="12". Three levels, least specific first:DEFAULTS, thenwindow.VelocityMarqueeSettings = { cruise: 90 }declared before the engine, then the box's own attributes. - From JavaScript:
VelocityMarquee.mount(el, overrides)returns{ element, track, settings, speed(), lean(), period(), copies(), measure(), play(), pause(), destroy() }, and hands back the existing handle for a box already mounted.VelocityMarquee.destroy(el),VelocityMarquee.auto(root)andVelocityMarquee.defaultscomplete it. Events, all bubbling, withelementin thedetail:velocitymarquee:ready(period,copies,still),velocitymarquee:measure(period,copies,width,reason),velocitymarquee:enterandvelocitymarquee:leave(speed), andvelocitymarquee:reverse(direction,speed) each time the ribbon turns round. - It costs nothing off screen. An
IntersectionObservertakes a ribbon out of the frame loop the moment it leaves the window, and the loop itself stops when the last one leaves. Measured: 144 writes on the strip in 1.2 s on screen, 0 off screen. - **Under
prefers-reduced-motion: reduce** the ribbon stands still, makes no copy at all and leaves the page exactly as the stylesheet laid it out, with no transform of any kind. Setdata-velocity-marquee-reduced-speedto keep it crawling instead; there is no lean on that path whateverskewsays. The media query is listened to, so turning the setting on or off answers without a reload. - Touch: a finger scrolling the page drives the ribbon exactly like a wheel, since both move the page. Nothing answers the pointer itself, so a link inside the ribbon stays a link.
- The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own content in their place whenever they want, in the page's language.
- 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.
- Mark the box, not the section:
-
Avoid the pitfalls
- The stylesheet matters. Without
velocity-marquee.cssthe copies stack instead of lining up, the engine measures nothing, leaves the ribbon still and says so in the console after a second and a half, which is long enough for a sheet that is merely late to arrive. - A ribbon in a grid or a flex row needs the box to be given a width by its parent. The stylesheet asks for that with
contain: inline-size: the strip inside is as wide as every copy laid end to end, and without containment anautogrid column would take that width, which is thousands of pixels, and then ask for more copies still. A grid that holds a ribbon also wantsminmax(0, 1fr)rather than the defaultauto. - No width is set on the box by the component. A full bleed ribbon made of negative margins keeps working. A ribbon that must be exactly as wide as something else gets its width from your own class.
- The space between two items belongs to the items.
paddingormarginon each item repeats with the copies, so the rhythm holds at the join. A flexgapon the row does not: there is no gap between the last item of one copy and the first of the next. Either move the space onto the items, or give--velocity-marquee-gapthe same length; the engine warns once when it sees a gap it cannot follow. - The ribbon needs something to clip it. The box is
overflow: hiddenin the component CSS. An ancestor withoverflow: visibleand a fixed height is not enough on its own. - **Do not put
data-velocity-marqueeon a section that also holds your headings.** The whole content of the box becomes the ribbon when no track is marked. Mark the row. - Two ribbons on one page are independent, and share a single frame loop and a single reading of the scroll, so a second one costs almost nothing.
- **An
idinside the ribbon travels no further than the original row**: copies have their ids stripped and their focusable elements taken out of the tab order, because a document with the same id three times is broken. Do not count on a copy being reachable. - A pointer state never survives the paste (AB-41): a hover colour on a link inside the ribbon lives in the CSS of the component or in your own class, never in a Webflow class variant.
- The stylesheet matters. Without
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
Velocity marquee is a scroll animation for Webflow and vanilla JavaScript, built on GSAP 3. Also called scroll velocity marquee, marquee speed on scroll or marquee scroll direction.
A ribbon of content runs on without end, and the page scroll drives it: it cruises on its own when nothing is happening, gains speed as the reader scrolls down, runs back the other way when the reader scrolls up, and leans a little while it is being pulled, standing upright again the moment it settles. The repeat is seamless at any window width and with any content: the row is written once, the engine repeats it as many times as the width needs, reads the distance between two copies from the page itself and wraps a single transform on that distance, so there is no jump at the join and no drift after any number of turns. It is real text the whole time, selectable and searchable, read once by a screen reader and once only, and nothing runs at all while the ribbon is off screen or when the reader asks for reduced motion. Pattern type velocity marquee, scrolling ticker, infinite scroller, speed reactive text band.
Updated
Questions
Add the custom attribute data-velocity-marquee, with an empty value, to the box that clips the ribbon, and data-velocity-marquee-track to the row inside it that holds the content, written once. The engine copies that row as many times as the width needs. Settings are more attributes on the same box, such as data-velocity-marquee-cruise="90".
No ScrollTrigger. It uses the GSAP 3.12.5 core only, from the CDN, for its ticker and quickSetter, and reads window.scrollY itself once a frame. That is also why Webflow.destroy() cannot take the effect down.
data-velocity-marquee-cruise is the speed when the page stands still, 46 pixels a second by default, and data-velocity-marquee-boost, 1.5 by default, is the share of the scroll speed handed to the ribbon: lower and the scroll barely shows, higher and a flick of the wheel throws the ribbon across. data-velocity-marquee-max-speed is the ceiling, 2400 pixels a second by default.
Yes, and it does by default: reverse is true, so scrolling up runs the ribbon backwards. Set data-velocity-marquee-reverse="false" and the page only ever pushes it forward. data-velocity-marquee-direction, left or right, sets which way it runs at rest.
A flex gap on the row does not repeat at the join: there is no gap between the last item of one copy and the first of the next. Put the space on the items as padding or margin, or give --velocity-marquee-gap the same length as the gap.