Pixel ribbon
A ribbon of pixels trails the cursor, soft and liquid: it lies on the path the pointer has just taken, lags a little behind it and wobbles when the gesture turns.
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 tutorial -
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-pixel-ribbon Marks the section the ribbon runs over. It takes no value: leave the value field empty. That section is what the canvas is laid over, what catches the pointer and what carries the settings. Without it on any section of the page, the embed marks the section it was pasted into. data-pixel-ribbon-state Written by the engine on the section: live while the ribbon is drawn, idle at rest, still under reduced motion, held for a frozen ribbon. Never written by hand. [ 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-pixel-ribbon-color #ffab1a Colour of the pixels of the ribbon. Options: any CSS colour; taken out of this block, --pixel-ribbon-color on the section takes over, then the amber #ffab1a. data-pixel-ribbon-opacity 1 Opacity of the whole ribbon, 0 to 1. data-pixel-ribbon-cell 6 Side of one pixel of the grid the ribbon is drawn on, in px. data-pixel-ribbon-size clamp(9px, 0.82vw, 14px) Radius of the ribbon at its head, any CSS unit; the tail is thinner. data-pixel-ribbon-life 0.75 How long the ribbon takes to deflate once the pointer slows, in seconds. data-pixel-ribbon-burst 2.2 Reach of the drops a tap throws, in times size.
Two files, 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
PixelRibbon.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-pixel-ribbon-color #ffab1a Colour of the pixels of the ribbon. Options: any CSS colour; taken out of this block, --pixel-ribbon-color on the section takes over, then the amber #ffab1a. data-pixel-ribbon-opacity 1 Opacity of the whole ribbon, 0 to 1. A faint trace over the words A solid ribbon that hides what it crosses data-pixel-ribbon-cell 6 Side of one pixel of the grid the ribbon is drawn on, in px. A fine grain, the ribbon reads as a smooth stroke; more cells to fill each frame Chunky blocks, the pixels show plainly and the rim steps data-pixel-ribbon-size clamp(9px, 0.82vw, 14px) Radius of the ribbon at its head, any CSS unit; the tail is thinner. A thin thread; under about one cell nothing is drawn A fat ribbon that swallows the words it crosses data-pixel-ribbon-life 0.75 How long the ribbon takes to deflate once the pointer slows, in seconds. It thins out almost at once and stays short It stays swollen and folds back slowly data-pixel-ribbon-burst 2.2 Reach of the drops a tap throws, in times size. The drops barely leave the finger The drops fly far before the springs pull them back -
Drive it from JavaScript
When the section arrives after load, when a tap has to be played from code, or when the ribbon has to be taken off at once.const element = document.querySelector('[data-pixel-ribbon]'); const ribbon = PixelRibbon.mount(element); // the section auto() mounted, or a new one ribbon.burst(240, 180); // drops thrown from that point of the section ribbon.clear(); // and the ribbon taken off at once // When the section leaves the page: ribbon.destroy(); // removes the canvas and the listeners
-
Fit it to your page
- One section, one attribute. Put
data-pixel-ribbonon the section the ribbon runs over. The script adds a<canvas>of its own inside it (classpixel-ribbon-layer,position: absolute; inset: 0; pointer-events: none, written inline as well as in the stylesheet, so the canvas is out of the flow before the stylesheet arrives) and switches a static section toposition: relative. The canvas goes over the content, which is what makes the effect, and never takes a click: links and buttons underneath keep working. - The sensitive area is the section, not the window: coordinates are relative to its
getBoundingClientRect(). An empty section is 0px tall and catches no pointer; give it content or a height. - One setting for every ribbon of the page, without opening the file: declare
window.PixelRibbonSettings = { size: '18px' }in a script placed beforepixel-ribbon.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-pixel-ribbon-*attribute on the section itself (data-pixel-ribbon-cell="8"), so one section can still differ from the page-wide setting. The colour has one more level: without acoloranywhere, the section's--pixel-ribbon-coloris read. - Touch: a finger drag traces the ribbon too, through touch events, and the page still scrolls under it; the ribbon is drawn in section coordinates, so it stays where the finger passed. A short tap that barely moves throws the chain out from the point as drops, the springs slack at first, then firming up and pulling the drops back in.
- **
prefers-reduced-motion**: no ribbon at all, not even a still dot. The pointer is the user's own cursor, and a mark that follows it is motion by nature. The engine listens to the setting and stops or starts with it. - Cost at rest is nothing: the loop only runs while pixels are on screen, sleeps once the ribbon has deflated, and stops in a hidden tab. The section carries
data-pixel-ribbon-state:livewhile the loop runs,idleat rest,stillunder reduced motion,heldfor a frozen ribbon. - From JavaScript:
PixelRibbon.mount(el, overrides)returns the instance, which carrieselement,settings(the lengths in px),resize(),burst(x, y)(the drops of a tap at a point of the section),clear(),frames()andisRunning()(rendered frames since mount, and whether the loop runs, for a test that the loop sleeps), anddestroy().destroy()takes away the canvas, every listener, the size watch and the loop, and gives back apositionit had set, so the markup is as it was and mounting again starts clean.PixelRibbon.destroy(el)does the same from the element. In React,mountinuseEffectanddestroyin its cleanup; the double run of StrictMode then leaves one ribbon, not two. freeze()holds a ribbon as a still picture: what is on screen is taken off, then the next ribbon drawn is held a tenth of a second after the pointer pauses, lying on its path instead of folding in, and the loop stops, so a held ribbon costs nothing.unfreeze()takes the held ribbon off and lets the next one live out its normal life.PixelRibbon.freeze(el)andPixelRibbon.unfreeze(el)do the same from the element. It exists so a still image of the effect can be taken, a thumbnail that has to show a ribbon at rest: a page that only wants the ribbon never calls it.- 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.
- One section, one attribute. Put
-
Avoid the pitfalls
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a section that already carries a ribbon returns the existing one untouched. A page-wide block therefore goes inwindow.PixelRibbonSettings, whichauto()reads itself. - **Content stacked above
z-index: 3covers the ribbon.** The canvas sits atz-index: 3inside the section. Content of the section with its own stacking context higher than that (a sticky header atz-index: 10, for one) is drawn over the ribbon, which then passes under it. - **Mouse and pen only for
pointermove.** A touch pointer is ignored there on purpose, touch going through touch events; a page that fakes pointer events for a test has to give thempointerType: 'mouse'. - The ribbon is not a cursor. The system cursor stays where it is; hide it yourself with
cursor: noneon the section if the ribbon should be the only mark, and keep it on for links.
- **Settings handed to
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
Pixel ribbon is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called pixel cursor trail, liquid cursor trail or blob cursor effect.
A ribbon of pixels trails the cursor, soft and liquid: it lies on the path the pointer has just taken, lags a little behind it and wobbles when the gesture turns. Under it is a chain of blobs, each one tied by a spring to where the pointer was a moment earlier, summed into one field on a pixel grid, so they melt into a single shape; the pixels at its rim are drawn smaller, a fringe that keeps changing shape as the chain moves. The ribbon swells with the travel of the pointer, and when the pointer stops it folds back into its head and deflates, the tail last. Nothing but pixels, no picture to load: one canvas over the section, clicks pass through, and the loop only runs while something is on screen. A finger drag traces it too and a tap throws the chain out as drops that pull back in; under reduced motion there is no ribbon at all.
Updated
Questions
Paste the component: the trail runs over the section that carries the custom attribute data-pixel-ribbon, and nowhere else. Its settings are attributes on that same section, for instance data-pixel-ribbon-cell="8". An empty section is 0px tall and catches no pointer, so give it content or a height.
Neither. It runs on a 2D canvas with no library: a chain of blobs on springs, summed into one field on a pixel grid so they melt into a single shape. The loop only runs while pixels are on screen.
Change data-pixel-ribbon-size, the radius of the ribbon at its head, which accepts any CSS unit and defaults to clamp(9px, 0.82vw, 14px). A lower value gives a thin thread, and under about one cell nothing is drawn. A higher value gives a fat ribbon that swallows the words it crosses.
No. The system cursor stays where it is, and the canvas never takes a click, so links and buttons underneath keep working. If the ribbon should be the only mark, set cursor: none on the section yourself and keep the cursor on for links.
Yes. A finger drag traces the ribbon while the page still scrolls under it, and a short tap throws the chain out as drops that the springs pull back in. Under prefers-reduced-motion there is no ribbon at all, not even a still dot.