Pixel image trail
Images settle along the path of the cursor, then leave again.
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-trail Marks the section the effect plays on. It takes no value: leave the value field empty. data-pixel-trail-images Marks the hidden box holding the images the trail draws. Without it the engine takes every image inside the section, wherever it sits. [ Optional ] data-images A comma separated list of picture addresses, read only when the section holds no img at all. With images in the markup it is ignored. [ 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-accent #ffab1a Flash colour carried by the pixel front. Options: any CSS colour; taken out of this block, --pixel-trail-accent on the section then the page's --accent take over. data-width clamp(170px, 15vw, 300px) Width of one card laid down along the cursor, any CSS unit. data-height clamp(222px, 19.6vw, 392px) Height of that same card, the picture being cropped to fill it. data-cell 18 Side of one square the image is composed from, kept in px so the grain is the same on every screen. data-sweep 0.35 How much the order of the squares follows a diagonal rather than chance, 0 to 1. data-appear 0.32 How long a card takes to compose square by square, in seconds. data-hold 0.6 How long the card stays whole before it breaks up, in seconds. data-vanish 0.46 How long the card takes to break up square by square, in seconds. data-front 0.06 How long a square stays lit by the accent as the front passes over it, in seconds. data-front-intensity 1 Strength of the accent light carried by that front, 0 to 1. data-flat 1 How solid the lit squares are, 0 to 1. data-min-distance 115 Distance the cursor travels between two cards, any CSS unit. data-drift 26 How far a card glides along the direction of travel while it lives, any CSS unit. data-max-rotation 9 Random tilt given to each card, in degrees. data-pool 16 Cards alive at the same time.
Two files and their pictures (7), 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
PixelTrail.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-accent #ffab1a Flash colour carried by the pixel front. Options: any CSS colour; taken out of this block, --pixel-trail-accent on the section then the page's --accent take over. data-width clamp(170px, 15vw, 300px) Width of one card laid down along the cursor, any CSS unit. Small stamps, the page shows through between them Wide cards that cover the section data-height clamp(222px, 19.6vw, 392px) Height of that same card, the picture being cropped to fill it. Squat, letterbox cards Tall portraits, never a squashed image data-cell 18 Side of one square the image is composed from, kept in px so the grain is the same on every screen. A fine grain, many more squares to draw Chunky blocks, cheaper to draw data-sweep 0.35 How much the order of the squares follows a diagonal rather than chance, 0 to 1. The image gathers at random A clean diagonal front crossing the card data-appear 0.32 How long a card takes to compose square by square, in seconds. The picture snaps into place A slow build-up under the cursor data-hold 0.6 How long the card stays whole before it breaks up, in seconds. Under 0.4 only debris shows, the picture is never quite there The images linger and stack up along the path data-vanish 0.46 How long the card takes to break up square by square, in seconds. The card blinks out A slow dissolve trailing behind the cursor data-front 0.06 How long a square stays lit by the accent as the front passes over it, in seconds. Barely a coloured edge A wide accent band leading the composition data-front-intensity 1 Strength of the accent light carried by that front, 0 to 1. The accent is only a hint Over 0.6 the accent hides the image under it data-flat 1 How solid the lit squares are, 0 to 1. A light that fades behind the front At 1 real squares of flat colour, which then turn into the picture data-min-distance 115 Distance the cursor travels between two cards, any CSS unit. A dense trail, cards almost touching Rare cards, far apart data-drift 26 How far a card glides along the direction of travel while it lives, any CSS unit. The card stays where it was laid down It slides away behind the cursor data-max-rotation 9 Random tilt given to each card, in degrees. A tidy stack, every card upright Cards thrown down at an angle data-pool 16 Cards alive at the same time. The oldest card is recycled early and the trail stays short A longer trail, more memory held -
Drive it from JavaScript
When the sources are not <img> elements, or when you want to react to what the component does.const element = document.querySelector('[data-pixel-trail]'); element.addEventListener('pixeltrail:spawn', (e) => { console.log(e.detail); }); const trail = PixelTrail.mount(element); // the section auto() mounted, or a new one // Sources that are not <img> elements: here, two canvases painted in code. function paint(color) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = 600; const context = canvas.getContext('2d'); context.fillStyle = color; context.fillRect(0, 0, 600, 600); return canvas; } trail.setSources([paint('#f4ece0'), paint('#15100c')]); // When the section leaves the page: trail.destroy(); // removes the canvas and the listeners
-
Fit it to your page
- One setting for every trail of the page, without opening the file: declare
window.PixelTrailSettings = { hold: 0.8 }in a script placed beforepixel-trail.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-*attribute on the element itself, so one section can still differ from the page-wide setting. - Dropping in your own images: put them in a container marked
data-pixel-trail-imagesinside the element. Any size, any aspect: every source is redrawn into one card ofwidth × heightbefore the trail ever sees it, centred and cropped the wayobject-fit: coverwould. Proportions are kept, never squashed, so what does not fit is cut: a 1600 × 400 banner arrives as a narrow column taken from its middle. The card is drawn at the device pixel ratio, capped at 2, so a source underwidth × 2byheight × 2is upscaled and reads soft. To frame differently, change the card rather than the images:data-widthanddata-heighton the element. - With no
<img>in the element at all, a comma separated list of addresses indata-imageson the element is loaded instead. - Minimal structure: a container in
position: relative, which the script gives a<canvas>of its own (classpixel-trail-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). A static container is switched torelativeby the script. The canvas goes over the content, and that is what makes the effect; putting it underneath gives a trail that disappears behind the headings. - The sensitive area is the container, not the window: coordinates are relative to its
getBoundingClientRect(). - The component drops nothing while it has no source, so loading the images cannot produce a trail full of holes.
- Touch: it works with a finger without changing anything (
pointermovecovers touch), but a trail on mobile is expensive: lowerpoolandwidth/height, or only turn it on for a fine pointer (matchMedia('(pointer: fine)')). - **From JavaScript, beyond
mount,setSourcesanddestroy**: the apiPixelTrail.mount(el, overrides)returns also carrieselement,settings,resize()andrender()to draw one frame by hand. A secondmount(el, { sources })on a mounted element hands the sources over. The element emitspixeltrail:spawnwith{ index, count }asevent.detail.destroy()takes away the canvas, the pointer and resize listeners, the size watch and the render loop, and gives back apositionit had set, so mounting again on the same element starts clean. In React,mountinuseEffectanddestroyin its cleanup; the double run of StrictMode then leaves one trail, not two. freeze()holds the trail as a still picture: a card stops ageing the moment it is whole, so none of them drifts or leaves, and the render loop stops as soon as the last one has composed, which makes a held scene cost nothing.unfreeze()hands the trail back to the clock and the cards it was holding leave at once.PixelTrail.freeze(el)andPixelTrail.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 trail standing still: a page that only wants the trail never calls it.- **
prefers-reduced-motion**: no pixels, the images appear and go out in a plain fade, and the trail stays short. - 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 setting for every trail of the page, without opening the file: declare
-
Avoid the pitfalls
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on an element that already carries a trail returns the existing one, taking only its sources. A page-wide block therefore goes inwindow.PixelTrailSettings, whichauto()reads itself; handed to a latermount()call it is silently dropped, and the settings block ends up looking decorative. - Without the stylesheet, the source images show. The engine keeps its own canvas out of the flow, but hiding
[data-pixel-trail-images]is the job ofpixel-trail.css: load the script alone and the seven images appear in the section at their natural size. Link the stylesheet, or hide the container yourself. - The image addresses of the markup are relative,
images/…, so they point at theimages/folder next to the page holding the markup. Moved into another page or a framework route, they have to be pointed at wherever your images are served from. - An image that never loads is skipped after 8 s, with no error and no warning. When none loads, the trail stays silent: check the addresses first.
- Text painted into your own canvas sources needs its fonts loaded first: wait for
document.fonts.load()before painting, otherwise the text comes out in the fallback font without a single error. Wait withPromise.allSettled, notPromise.all: a font file that never arrives rejects, andPromise.allthen never paints anything. make-cards.html, which bakes the seven cards of images/, waits for Inconsolata that way.
- **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 image trail is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called image trail, pixel trail cursor or cursor image trail effect.
Images settle along the path of the cursor, then leave again. None of them appears in one piece: each one composes cell by cell, like an image being loaded, and decomposes the same way as it goes out. The pixel front lights up in the accent tint just before laying the image down, and again just before putting it out, which reads as a render in progress rather than a plain fade. An image is only dropped once a certain distance has been travelled, so the trail follows the speed of the gesture: slow, it leaves a few crisp images; fast, it leaves a scarf of fragments. Each image drifts a little along the direction of travel and carries a slight rotation. Everything is painted into a single canvas laid over the content; each image also keeps its own, off-screen, where only the cells that change get redrawn. An image trail pattern, digital-decomposition flavour.
Updated
Questions
Yes. You paste the section on the canvas, and each setting is a custom attribute on the element that carries data-pixel-trail, for instance data-cell="12". Add Geist and Geist Mono to the site's fonts before pasting, because the Designer drops a font the site does not have.
No. The engine has no dependency and no build step. Every image is painted cell by cell into a single canvas laid over the section.
Put your <img> tags in a container marked data-pixel-trail-images inside the section. Any size and any aspect works: each picture is cropped to fill one card, the way object-fit: cover would, and the card is sized with data-width and data-height. A source smaller than twice the card in each direction is upscaled and reads soft.
Raise data-hold, the time in seconds a card stays whole before it breaks up. The default is 0.6. Under 0.4 only debris shows and the picture is never quite there, while a higher value lets the images linger and stack up along the path.
Yes, a finger traces it with no change, because the trail listens to pointermove. A trail on mobile is expensive though, so lower pool (16 cards by default) and the card width and height, or turn it on only for a fine pointer with matchMedia('(pointer: fine)').