Hover list
A list of projects or services, one row per entry: a number, a title, a category, a year.
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 pictures of the rows are in the markup, hidden: the stylesheet takes them out of the layout, and without it they show in the rows. The box that holds the rows keepsoverflow: visible, or it clips the picture floating over it. -
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-hover-list Marks the box that holds the rows. It takes no value: leave the value field empty. That box is what the picture floats in, what the effect is mounted on and what carries the settings. Without it on any box of the page, the embed marks the parent of the first row it finds next to itself, and says so in the console when there is none. data-hover-list-item Marks one row, inside that box. It takes no value. A row is what the pointer runs over and what takes the focus, so in the preview it is the link itself. Without it on any row, nothing is mounted and the console says so. [ Required ] data-hover-list-title Marks the title of a row, the words that slide while the row is live. It takes no value. Without it the row still shows its picture and dims the others; only the slide is missing. [ Optional ] data-hover-list-image Marks the hidden picture of a row, an Image inside it that the script reads and never shows. It takes no value. Without it the first image of the row is taken; a row with no image shows an empty box and says so once in the console. [ Optional ] data-hover-list-state Written by the engine: idle or live on the box, live on the row under the pointer and dim on the others, taken off at rest. Never written by hand; the stylesheet reads it for the colour of the live number. [ 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-hover-list-accent #ffab1a Flash colour of the turning squares, the amber #ffab1a by default. Options: any plain CSS colour; taken out of this block, --hover-list-accent on the list then the page's --accent take over. data-hover-list-width clamp(170px, 15vw, 300px) Width of the floating picture, any CSS unit. data-hover-list-height clamp(222px, 19.6vw, 392px) Height of the floating picture, any CSS unit. data-hover-list-shift clamp(28px, 3vw, 56px) How far to the right of the pointer the picture sits, any CSS unit. data-hover-list-lag 0.55 How long the picture takes to catch up with the pointer, in seconds. data-hover-list-top-speed 1400 Pointer speed, in px per second, at which the lean and the stretch are at their largest. data-hover-list-skew 0 Largest lean of the picture, in degrees, reached at top speed. data-hover-list-stretch 0.08 Largest stretch along the way of travel, 0 to 1. data-hover-list-cell 16 Side of one square of the turn, kept in px so the grain is the same on every screen. data-hover-list-swap 0.85 How long a picture takes to turn into the next one, in seconds. data-hover-list-ease power2.inOut Pace of the turn across the squares, a GSAP ease name. data-hover-list-flash 1 Strength of the accent light on the squares at the front of a turn, 0 to 1. data-hover-list-front 0.1 Share of the squares lit at the front of a turn, 0 to 1. data-hover-list-flat 1 How solid the lit squares are, 0 to 1. data-hover-list-fade 0.6 How long the picture takes to grow in on the first row and to shrink out on leaving, in seconds; on leaving it fades while its squares go out, so never faster than most of a swap. data-hover-list-slide clamp(12px, 1.4vw, 24px) How far the title of the live row moves to the right, any CSS unit. data-hover-list-dim 0.3 Opacity of the other rows while one is live, 0 to 1.
Two files and their pictures (6), no build step. It also needs GSAP 3 on the page, loaded with the tag in step 1. Without it the rows stay exactly as the page styled them, and no picture shows.
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
HoverList.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-hover-list-accent #ffab1a Flash colour of the turning squares, the amber #ffab1a by default. Options: any plain CSS colour; taken out of this block, --hover-list-accent on the list then the page's --accent take over. data-hover-list-width clamp(170px, 15vw, 300px) Width of the floating picture, any CSS unit. A thumbnail that reads as a cursor A picture that covers the rows it floats over data-hover-list-height clamp(222px, 19.6vw, 392px) Height of the floating picture, any CSS unit. A landscape strip A tall portrait, which is what the demo's pictures are data-hover-list-shift clamp(28px, 3vw, 56px) How far to the right of the pointer the picture sits, any CSS unit. Centred on the pointer, hiding the word under it Held clear of the pointer, further into the row data-hover-list-lag 0.55 How long the picture takes to catch up with the pointer, in seconds. Glued to the pointer, no trail A picture that drifts in well after the pointer has stopped data-hover-list-top-speed 1400 Pointer speed, in px per second, at which the lean and the stretch are at their largest. A flick leans the picture at once Only a fast sweep across the screen leans it data-hover-list-skew 0 Largest lean of the picture, in degrees, reached at top speed. At 0 no lean at all, the picture only follows Past 25 the picture reads as a parallelogram at every move data-hover-list-stretch 0.08 Largest stretch along the way of travel, 0 to 1. At 0 the picture keeps its size Past 0.4 the picture smears data-hover-list-cell 16 Side of one square of the turn, kept in px so the grain is the same on every screen. A fine grain, more squares to paint Chunky blocks, cheaper to paint data-hover-list-swap 0.85 How long a picture takes to turn into the next one, in seconds. A snap from one picture to the next A slow dissolve where two rows show at once data-hover-list-ease power2.inOut Pace of the turn across the squares, a GSAP ease name. none keeps one even pace from the first square to the last power2.inOut starts gently, rushes the middle and lets the last squares settle slowly data-hover-list-flash 1 Strength of the accent light on the squares at the front of a turn, 0 to 1. At 0 no light, a plain dissolve Past 0.7 the whole turn is tinted in the accent data-hover-list-front 0.1 Share of the squares lit at the front of a turn, 0 to 1. A thin line of light runs across the picture Most of the picture is lit before it settles data-hover-list-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-hover-list-fade 0.6 How long the picture takes to grow in on the first row and to shrink out on leaving, in seconds; on leaving it fades while its squares go out, so never faster than most of a swap. The picture pops in A slow arrival, which lags behind a quick pass data-hover-list-slide clamp(12px, 1.4vw, 24px) How far the title of the live row moves to the right, any CSS unit. At 0 the title stays put A title that leaves its column data-hover-list-dim 0.3 Opacity of the other rows while one is live, 0 to 1. The other rows all but vanish At 1 nothing dims, only the live number lights -
Drive it from JavaScript
When the rows change after load, when something else than the pointer has to show a picture, or when the page reacts to the row that is live.const element = document.querySelector('[data-hover-list]'); element.addEventListener('hoverlist:show', (e) => { console.log(e.detail); }); const list = HoverList.mount(element); // the box auto() mounted, or a new one list.show(2); // poses the picture of the third row beside it list.hide(); // and takes it away element.addEventListener('hoverlist:change', (e) => { console.log('row', e.detail.from, 'to', e.detail.to); }); // When the list leaves the page, or before its rows are rendered again: HoverList.destroy(element); // removes the floating box and clears every state
-
Fit it to your page
- Structure: a box marked
data-hover-list, and inside it one row per entry markeddata-hover-list-item, each row holding its title markeddata-hover-list-titleand one<img>markeddata-hover-list-image, hidden by the stylesheet. In the reference markup the row is a link, so the whole line is hoverable and one Tab stop reads the whole entry. Anything else in the row (number, category, year) is yours. - **The picture of a row is the
<img>inside it.** Without thedata-hover-list-imagemark, the first<img>of the row is taken. A row with no picture, or whose picture never loads, shows an empty box and says so once in the console. - One setting for every list of the page, without opening the file: declare
window.HoverListSettings = { lag: 0.8 }in a script placed beforehover-list.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-hover-list-*attribute on the list itself (data-hover-list-lag="0.8"), so one list can still differ from the page-wide setting. - The size of the picture is a setting, not a class. The floating box is drawn by the script and has no box of its own in the page, so
widthandheightare values the script reads, in any CSS unit. The rows, on the other hand, are real boxes: their type, padding and hairlines are classes. - What the script writes.
data-hover-list-stateon the list,idleorlive; on each row,livefor the row under the pointer anddimfor the others, taken off at rest. The stylesheet reads the row state for the colour of the number; the dimming and the slide of the title are tweens. - **From JavaScript, beyond
mount,autoanddestroy**: the api carrieselement,settings,rows,show(index)which poses the picture beside a row,hide(),resize()andisVisible(). The list emitshoverlist:ready{ rows, pictures }once the pictures are in,hoverlist:show{ index, row }when the picture appears,hoverlist:change{ from, to, row }when it turns into another row's, andhoverlist:hide{ index }when it leaves. All bubble. - **
destroy()leaves the list as it was**: the floating box removed, the listeners gone, the states taken off, no opacity or transform left inline. - Keyboard: a row that takes the focus gets the picture posed at its right end, and Tab through the rows turns the picture from one to the next as the pointer would. Leaving the list takes it away.
- Touch: there is no pointer to follow, so a tap on a row poses the picture beside that row, held inside the window; a tap anywhere else takes it away. The link of the row still does what a link does. On a list too narrow to hold the words and the picture side by side (under 2.2 times
width), the posed picture goes under the row instead, or above it when the window has no room left under the row, so the title it belongs to stays readable; the same goes for the keyboard. - **
prefers-reduced-motion**: the picture never follows and never leans. It is posed beside the row under the pointer, turns into the next picture at once, and only fades in and out. - The words of the markup are the preview's, and they ship as they are. Put the user's own rows 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: a box marked
-
Avoid the pitfalls
- GSAP 3 has to be on the page. The engine waits ten seconds for
window.gsap, then says in the console that it never came. The Webflow embed carries the CDN tag; a plain page needs the tag beforehover-list.js. - Without the stylesheet, the source pictures show in the rows. Taking the hidden
<img>out of the layout is the job ofhover-list.css; the floating box itself is kept out of the flow inline, so it never pushes the rows down. - **The list needs
position: relative**, or the picture is placed against the nearest positioned ancestor. The stylesheet sets it, and the script sets it too when it finds the list static. - The image addresses of snippet.html are relative,
images/…, so they point at theimages/folder next to the page holding the markup, which is where the zip puts them. Moved into another page or a framework route, they have to be pointed at wherever your pictures are served from. The Webflow paste and the markup of the AI prompt carry the same pictures by their address on the Webflow CDN instead. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a list that already carries the effect returns the existing one. A page-wide block goes inwindow.HoverListSettings, whichauto()reads itself. - **The accent 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(). - **The picture is drawn at the size it is told, cropped like
object-fit: cover.** Pictures of different ratios in the same list are all cropped towidthbyheight; pick the ratio of the list, not of each picture. - **A row with
overflow: hiddenclips nothing here**, because the floating box is a child of the list, not of the row. The list's own overflow, on the other hand, would clip it: leave the list withoverflow: visible.
- GSAP 3 has to be on the page. The engine waits ten seconds for
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
Hover list is a cursor animation for Webflow and vanilla JavaScript, built on GSAP 3. Also called hover list image follow cursor, project list hover image or image follow cursor.
A list of projects or services, one row per entry: a number, a title, a category, a year. When the pointer runs over a row, the picture of that row floats beside the pointer and follows it with a lag, so it trails a little behind and settles when the pointer stops. The faster the pointer, the more the picture stretches, lightly, along the way it travels; it does not lean unless a skew is asked for. Moving to another row turns the picture into the next one square by square, with the ink lighting the front of the turn, the same grain as the bank's other pixel effects. The live row slides its title a little and the other rows dim. Leaving the list dissolves the picture and shrinks it out. Built on GSAP 3: quickTo movers for the lag and the lean, a ticker for the speed, one tween for each turn of the squares. On the keyboard, the picture is posed beside the focused row; on touch, beside the tapped row; under reduced motion it is posed, never follows, and only fades.
Updated
Questions
Paste the component, or mark your own rows: data-hover-list on the list, data-hover-list-item on each row, data-hover-list-title on its title and one image per row marked data-hover-list-image. Settings are custom attributes on the list, for instance data-hover-list-lag="0.8", where a higher lag lets the picture trail further behind the pointer.
Yes, GSAP 3 with no plugins. The Webflow embed carries the CDN tag for 3.12.5, and a plain page needs that tag before hover-list.js. The engine waits ten seconds for window.gsap, then says in the console that it never came.
Use data-hover-list-width and data-hover-list-height, in any CSS unit. The defaults are clamp(170px, 15vw, 300px) wide and clamp(222px, 19.6vw, 392px) tall. The size is a setting, not a class, because the floating picture is drawn by the script, and every picture is cropped to that box like object-fit: cover.
Yes. A tap on a row poses the picture beside that row and a tap anywhere else takes it away, while the row's link still does what a link does. On the keyboard, the focused row gets the picture at its right end, and Tab turns it from one row's picture to the next.