Pixel wake
A wake of squares behind the cursor. The section is a fixed grid of squares with no gap between them, and the pointer turns over every square within its reach: a turned square takes a pale tint of the ink, and the moment it turns, open or shut, it carries a short flash of colour, which is what gives the trail its front.
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-wake Marks the section the wake runs over. It takes no value: leave the value field empty. The canvas is laid over that section and sized from it, so the section needs a height of its own. Without it on any element of the page, the embed marks the section it was pasted into. data-pixel-wake-state Written by the engine on the section: live while squares are on screen, idle at rest, still under reduced motion. 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-wake-color #ffab1a Colour of the squares, their tint and their flash, the amber #ffab1a by default. Options: any plain CSS colour; taken out of this block, --pixel-wake-color on the section takes over. data-pixel-wake-opacity 1 Opacity of the whole trail, 0 to 1, applied over tint and flash. data-pixel-wake-cell 15 Side of one square, kept in px so the grain is the same on every screen. data-pixel-wake-size clamp(34px, 3.2vw, 54px) Radius of the brush, any CSS unit, before the grain breaks its rim. data-pixel-wake-life 0.5 How long a square stays open once the brush has left it, in seconds, each square stretching or shortening it by its own spread. data-pixel-wake-tint 0.09 Share of colour in an open square, 0 to 1. data-pixel-wake-flash 0.5 Share of colour in the flash a square carries as it turns, open or shut, 0 to 1. data-pixel-wake-burst 2.2 Reach of the ripple a tap leaves on touch, 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
PixelWake.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-wake-color #ffab1a Colour of the squares, their tint and their flash, the amber #ffab1a by default. Options: any plain CSS colour; taken out of this block, --pixel-wake-color on the section takes over. data-pixel-wake-opacity 1 Opacity of the whole trail, 0 to 1, applied over tint and flash. A ghost of a wake At 1 the tint and the flash show at their own strength data-pixel-wake-cell 15 Side of one square, kept in px so the grain is the same on every screen. A fine grain, more squares to fill Chunky blocks, a coarser wake data-pixel-wake-size clamp(34px, 3.2vw, 54px) Radius of the brush, any CSS unit, before the grain breaks its rim. A thin line of squares A wide band that covers the words it passes over data-pixel-wake-life 0.5 How long a square stays open once the brush has left it, in seconds, each square stretching or shortening it by its own spread. A short wake that closes right behind the pointer A long wake that lingers across the page data-pixel-wake-tint 0.09 Share of colour in an open square, 0 to 1. Past 0.05 the body of the wake all but disappears, only the flashes show Past 0.3 the wake reads as a solid band that hides the words under it data-pixel-wake-flash 0.5 Share of colour in the flash a square carries as it turns, open or shut, 0 to 1. At 0 no front, a flat pale wake At 1 the front and the closing squares blink at full strength data-pixel-wake-burst 2.2 Reach of the ripple a tap leaves on touch, in times size. A small splash under the finger A ripple that opens most of a phone screen -
Drive it from JavaScript
When something other than the pointer should open squares, or when the section changes size without the window doing so.const element = document.querySelector('[data-pixel-wake]'); const wake = PixelWake.mount(element); // the section auto() mounted, or a new one wake.burst(200, 120); // a ripple of squares at 200, 120 in the section wake.resize(); // after the section changed size on its own // When the section leaves the page: wake.destroy(); // removes the canvas and the listeners
-
Fit it to your page
- Structure: one element marked
data-pixel-wake, usually a section. The script adds one canvas over it,pixel-wake-layer, that never takes a click, so links and buttons under the wake keep working. Anything inside the section is yours; the effect reads no class and no child. - The section needs a height of its own. The canvas is sized from the section, and an empty section is 0px tall and catches no pointer. The preview's section is
min-height: 100vh. - One setting for every section of the page, without opening the file: declare
window.PixelWakeSettings = { life: 0.8 }in a script placed beforepixel-wake.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-pixel-wake-*attribute on the section itself (data-pixel-wake-life="0.8"), so one section can still differ from the page-wide setting. - The colour can also come from the stylesheet: without a
colorsetting in the attributes or in the settings block, the custom property--pixel-wake-coloron the section is read, thenDEFAULTS.color. The engine writes its own copy ofDEFAULTSinto the Embed, so on a Webflow page the attribute is the way to change it per section. - What the script writes:
data-pixel-wake-stateon the section,livewhile squares are on screen,idleat rest,stillunder reduced motion. Nothing else: no inline size, no class on the section. - **From JavaScript, beyond
mount,autoanddestroy**: the api carrieselement,settings,resize(),burst(x, y)which opens a ripple at a point of the section,clear()which takes every square away at once,frames()andisRunning(), for a test that wants to see the loop sleep at rest, andfreeze()andunfreeze(), which hold the wake as a still picture, for a thumbnail. The effect emits no event. - **
destroy()leaves the section as it was**: the canvas removed, the listeners gone, the state attribute taken off, and the inlineposition: relativeremoved if the script set it. - Touch: a finger drag traces the wake, through touch events, so the page still scrolls under the finger and the wake stays where the finger passed. A short touch that barely moves (under 10px and 350ms) is a tap, and opens a ripple of squares,
bursttimessizewide, that closes square by square like the wake. - **
prefers-reduced-motion**: no wake at all, not even a still square. The pointer is the user's own, and a mark that follows it is motion by nature. The state readsstill, and turning the preference off brings the effect back without a reload. - The words of the markup are the preview's, and they ship as they are. Put the user's own words 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 element marked
-
Avoid the pitfalls
- The wake is drawn over the content, not under it. The canvas sits at
z-index: 3inside the section; an element of the section with a higher z-index of its own shows over the squares. Content that must stay above the wake takesposition: relativeand a z-index over 3. - **
overflow: hiddenon the section is not needed**: the canvas is the size of the section and never spills. A parent that clips does not clip the wake either. - **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.PixelWakeSettings, whichauto()reads itself. - **The colour goes to a canvas, which cannot parse
color-mix().** A colour written that way paints black without a word. Give it a plain colour, a hex or anrgb(). - **On a dark section, raise
tint.** The open square is a share of the ink over whatever is under the canvas; light squares on a dark ground need a lightcolorand usually a strongertint. Attint1 andflash1 the squares are solid ink. - A section taller than the screen costs its whole area. The canvas is one bitmap the size of the section, at up to twice the pixel density; the loop only runs while squares are on screen, but a section of several screens holds a large bitmap all the same.
- The wake is drawn over the content, not under it. The canvas sits at
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 wake is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called pixel grid hover effect, grid trail cursor effect or interactive pixel grid background.
A wake of squares behind the cursor. The section is a fixed grid of squares with no gap between them, and the pointer turns over every square within its reach: a turned square takes a pale tint of the ink, and the moment it turns, open or shut, it carries a short flash of colour, which is what gives the trail its front. Each square then closes on its own clock, its time stretched or shortened by a spread fixed per square and pushed back while the brush is still over it, so the patch behind the pointer comes apart square by square instead of fading in one block. The rim of the brush is broken by a grain fixed per square, and the brush is stamped along the whole segment, so a flick never leaves a dotted line. Nothing but squares, no picture to load: one canvas over the section, no library. On touch, a finger drag traces the same wake while the page still scrolls, and a tap opens a ripple of squares that closes the same way. The squares of the pixel swap, turned into a trail.
Updated
Questions
Yes. Once pasted, the effect runs on any section that carries the custom attribute data-pixel-wake, and it reads no class and no child, so everything inside the section is yours. Each setting is one more attribute on that section, for instance data-pixel-wake-life="0.8".
None. It is one canvas over the section with no library and no picture to load: a fixed grid of squares, each one closing on its own timer.
Raise data-pixel-wake-life, the time in seconds a square stays open once the brush has left it. The default is 0.5. Lower, the wake closes right behind the pointer, and higher, it lingers across the page.
In front. The canvas sits at z-index: 3 inside the section and never takes a click. Content that must stay above the squares takes position: relative and a z-index over 3.
Give it a light color and usually a stronger tint, the share of colour in an open square, which is 0.09 by default. Write the colour as a hex or an rgb(): it goes to a canvas, which cannot parse color-mix() and paints black instead.