Drag canvas
A wall of cards with no edge: pictures of a collection, projects, products.
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 tutorialGive the canvas a height: once mounted the cards are placed by the script and no longer push the box open. The size of a card is not a class here but thewidthandheightsettings, which the script hands to the stylesheet. And leave the transform of a card to the engine, which writes it every frame. -
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-drag-canvas Marks the box that holds the cards. It takes no value: leave the value field empty. That box is the canvas: what the hand drags, what the effect is mounted on and what carries the settings. It takes its height from the page. Without it on any box of the page, the embed marks the parent of every card it finds (data-drag-canvas-card). data-drag-canvas-card Marks one card, a direct child of the canvas. It takes no value. A card can be a link: a plain click still opens it, a drag never does. Without it on any card, the canvas takes its own children as the cards. [ Optional ] data-drag-canvas-image Marks the picture of a card, the element that slides inside it as the wall goes by. It takes no value. Without it the first image of the card is taken; a card with no image simply does not slide. [ Optional ] data-drag-canvas-state Written by the engine on the canvas once mounted: ready. Never written by hand; the stylesheet reads it to clip the canvas, keep the gesture and take the cards out of the flow. [ Optional ] data-drag-canvas-dragging Written by the engine on the canvas while a drag runs. Never written by hand; the stylesheet reads it for the closed hand. [ Optional ] data-drag-canvas-clone Written by the engine on the copies of the cards it adds to fill the wall, each one hidden from screen readers and out of the Tab order. Never written by hand. [ 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-drag-canvas-tint #15100c Colour of the veil laid on the cards away from the pointer, the ink in the cream and ink look. Options: any CSS colour; taken out of this block, --drag-canvas-tint on the canvas takes over. data-drag-canvas-veil 0.16 Strength of that veil, 0 to 1, on a card out of the pointer's reach. data-drag-canvas-width clamp(150px, 17vw, 300px) Width of a card, any CSS unit. data-drag-canvas-height clamp(188px, 21.25vw, 375px) Height of a card, any CSS unit. data-drag-canvas-gap clamp(10px, 1.2vw, 20px) Space between two cards, on both axes, any CSS unit. data-drag-canvas-stagger 0.5 How far every other column sits lower, as a share of one cell, 0 to 1. data-drag-canvas-friction 0.05 Share of its speed a thrown wall loses on each frame, 0 to 1. data-drag-canvas-ratio 1 How far the wall travels for one pixel of hand travel. data-drag-canvas-snap 0 Whether a throw comes to rest with one card in the middle of the canvas, 0 or 1. data-drag-canvas-wheel 1 Whether the wheel and the trackpad move the wall, 0 or 1. data-drag-canvas-lift 1.05 Scale of the card right under the pointer. data-drag-canvas-reach clamp(220px, 30vw, 520px) Radius around the pointer inside which cards lift and clear, any CSS unit. data-drag-canvas-parallax 0.05 How far a picture slides inside its card as the card crosses the canvas, as a share of the card, 0 to 0.2.
Two files and their pictures (12), 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
DragCanvas.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-drag-canvas-tint #15100c Colour of the veil laid on the cards away from the pointer, the ink in the cream and ink look. Options: any CSS colour; taken out of this block, --drag-canvas-tint on the canvas takes over. data-drag-canvas-veil 0.16 Strength of that veil, 0 to 1, on a card out of the pointer's reach. At 0 no veil at all, the cards only lift Past 0.4 the wall goes dark around the pointer data-drag-canvas-width clamp(150px, 17vw, 300px) Width of a card, any CSS unit. Small tiles, many on screen, more nodes on the page A few large cards, each one read on its own data-drag-canvas-height clamp(188px, 21.25vw, 375px) Height of a card, any CSS unit. Landscape cards Tall portrait cards, which is what the bank's reams are data-drag-canvas-gap clamp(10px, 1.2vw, 20px) Space between two cards, on both axes, any CSS unit. At 0 the cards touch and the wall is one surface A loose wall with a lot of page between the cards data-drag-canvas-stagger 0.5 How far every other column sits lower, as a share of one cell, 0 to 1. At 0 a plain grid, the rows straight At 0.5 a brick layout, each column half a card lower than the last data-drag-canvas-friction 0.05 Share of its speed a thrown wall loses on each frame, 0 to 1. A wall that glides a long way before it stops A wall that stops almost where the hand left it data-drag-canvas-ratio 1 How far the wall travels for one pixel of hand travel. Under 1 a heavy wall that lags behind the hand Over 1 a light wall that outruns the hand data-drag-canvas-snap 0 Whether a throw comes to rest with one card in the middle of the canvas, 0 or 1. At 0 the wall rests wherever its speed runs out At 1 every throw, wheel and arrow lands on a centred card data-drag-canvas-wheel 1 Whether the wheel and the trackpad move the wall, 0 or 1. At 0 the wheel scrolls the page as usual, over the canvas too At 1 the canvas keeps the wheel, and the page does not scroll while the pointer is on it data-drag-canvas-lift 1.05 Scale of the card right under the pointer. At 1 no lift; under 1 the card sinks instead Past 1.1 the lifted card covers its gap and touches its neighbours data-drag-canvas-reach clamp(220px, 30vw, 520px) Radius around the pointer inside which cards lift and clear, any CSS unit. Only the card under the pointer answers The whole canvas lifts a little, and the veil hardly shows data-drag-canvas-parallax 0.05 How far a picture slides inside its card as the card crosses the canvas, as a share of the card, 0 to 0.2. At 0 the pictures sit still and are not cropped A deep slide, and a picture cropped by twice that share on each axis -
Drive it from JavaScript
When a button has to bring a card to the middle, when the cards change after load, or when the page reacts to where the wall came to rest.const element = document.querySelector('[data-drag-canvas]'); element.addEventListener('dragcanvas:rest', (e) => { console.log(e.detail); }); const canvas = DragCanvas.mount(element); // the box auto() mounted, or a new one canvas.select(4); // throws the wall to rest on the fifth card canvas.moveBy(1, 0); // one card to the right, as the arrow key does element.addEventListener('dragcanvas:rest', (e) => { console.log(e.detail.index); // the card nearest the middle }); // When the canvas leaves the page, or before its cards change: DragCanvas.destroy(element); // removes the copies and gives back the markup as it was
-
Fit it to your page
- Structure: a box marked
data-drag-canvas, and inside it the cards, its direct children, each markeddata-drag-canvas-card. In the reference markup a card is a link holding one picture markeddata-drag-canvas-image. Anything can go in a card: a title, a price, a tag. Twelve cards is a good number; one is enough for the engine. - The canvas takes its height from the page. Mounted, the cards are placed by the script and no longer push the box open, so the box needs a height of its own: a full screen, a share of it, a fixed band.
- Cards without the mark: a box with no marked card takes its own children as the cards, scripts and styles left out.
- One setting for every canvas of the page, without opening the file: declare
window.DragCanvasSettings = { snap: 1 }in a script placed beforedrag-canvas.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-drag-canvas-*attribute on the canvas itself (data-drag-canvas-snap="1"), so one canvas can still differ from the page-wide setting. - The size of a card is a setting here, not a class. The wall is laid out by the script, cell by cell, so the script has to know the cell:
width,heightandgapare read in pixels and handed to the stylesheet, which sizes every card and every copy alike. What is inside a card stays the page's own. - A click is still a click. A press that travels less than six pixels opens the card's link. A drag never opens one, and a press on a wall still running stops it without opening the card under it.
- What the script writes. On the canvas:
data-drag-canvas-state="ready"once mounted,data-drag-canvas-draggingwhile a drag runs,tabindex="0"androle="group"when the markup gave none, and three custom properties,--drag-canvas-width,--drag-canvas-heightand--drag-canvas-tint. On each card: atransform, and--drag-canvas-shade, the strength of its veil. On each picture: atransform. The copies carrydata-drag-canvas-clone. - **From JavaScript, beyond
mount,autoanddestroy**: the api carrieselement,settings,cards(the cards of the markup),position()andspeed()as{ x, y },phase()(rest, hold, drag or glide),nodes(),moveBy(columns, rows),select(index)which throws the wall so that card comes to rest in the middle, andrefresh(). The canvas emitsdragcanvas:start{ element, x, y }when a drag begins anddragcanvas:rest{ element, x, y, column, row, index, card }each time the wall comes to rest. Both bubble. - **
destroy()leaves the canvas as it was**: the copies removed, the listeners and the size watch gone, the states, the custom properties and every transform taken off. - Keyboard: the canvas takes the focus, and the arrow keys move the wall by one card in their direction. Tab then reaches each card of the markup in its order, and a card that takes the focus is brought to the middle of the canvas. The copies never take the focus and are hidden from a screen reader, which reads each card once. Give the canvas an
aria-label. - Touch: a finger drags and throws the wall on both axes. There is no pointer to lift a card, so the wall is plain: no lift, no veil.
- **
prefers-reduced-motion**: the wall follows the hand and stops where it is let go; nothing glides, lifts or slides. The arrows, Tab andselect()put the wall in place at once. The veil still follows the pointer. - The words of the markup are the preview's, and they ship as they are. Put the user's own cards 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
- A canvas with no height shows nothing. The script says so once in the console. A percentage height needs a parent that has one.
- Without the stylesheet the cards are not placed. Taking them out of the flow and sizing them is the job of
drag-canvas.css; the script only writes transforms and custom properties. - A finger on the canvas never scrolls the page. The canvas keeps both axes of a swipe (
touch-action: none), or a drag would fight the page scroll. On a phone, a canvas as tall as the screen leaves nothing to scroll the page with: keep it shorter than the screen, or make it the whole page as the preview does. - **
wheelat 1 keeps the wheel too.** While the pointer is on the canvas the page does not scroll. Setdata-drag-canvas-wheel="0"on a canvas that sits in the middle of a long page. - 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 canvas that already carries the effect returns the existing one. A page-wide block goes inwindow.DragCanvasSettings, whichauto()reads itself. - The copies are clones of the markup. A listener added to a card with
addEventListeneris not on its copies: listen on the canvas and readevent.target.closest('[data-drag-canvas-card]'). Anidinside a card is taken off its copies. - Do not put a transform or a transition on a card. The script writes the card's transform every frame; a transition on it turns the drag into rubber. Style what is inside the card.
- Parallax crops. A picture is scaled by twice
parallaxto have room to slide, so its edges are cut. A picture that carries words near its edges wants a smallparallax, or 0. - Cards added or removed after mount are not seen. Destroy and mount again;
refresh()only reads the sizes again.
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
Drag canvas is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called infinite draggable grid, infinite draggable gallery or infinite draggable scrollable grid.
A wall of cards with no edge: pictures of a collection, projects, products. The hand grabs it anywhere, with the mouse, a pen or a finger, and drags it in every direction; released, the wall runs on with the speed it was given, slows down on its own and comes to rest. The cards of the page come round again on both axes, in a sequence shifted from row to row so the same card never sits beside itself. Every card is a real element, a link that still opens on a plain click, and the script keeps only as many on the page as one canvas can show: a card that leaves on one side is moved to the cell that enters on the other. Near the pointer the cards lift a little and stay clear while the others take a light veil of the tint, and each picture slides inside its card as the wall goes by, which gives the wall its depth. The wheel and the trackpad move it, the arrow keys move it by one card, and Tab reaches each card and brings it to the middle. No library. Under reduced motion the wall follows the hand and nothing runs on, lifts or slides.
Updated
Questions
Add the custom attribute data-drag-canvas to a box and data-drag-canvas-card to each card, its direct children. Give the box a height of its own, because the script places the cards and they no longer push it open. Settings are attributes on that box, for instance data-drag-canvas-snap="1", and the size of a card is one of them: data-drag-canvas-width and data-drag-canvas-height.
No. It has no library: the drag is written on pointer events, and the throw is a glide that fades the speed read from the last 100 ms of the drag. Each card is a real element, and a card that leaves on one side is moved to the cell that enters on the other, so the page only holds as many as one canvas can show.
Set data-drag-canvas-friction, the share of its speed a thrown wall loses on each frame, 0.05 by default. Lower lets it glide a long way before it stops, higher stops it almost where the hand left it. data-drag-canvas-snap="1" makes every throw, wheel and arrow land on a centred card.
It can. A finger on the canvas never scrolls the page, because the canvas keeps both axes of a swipe with touch-action: none: on a phone, keep it shorter than the screen or make it the whole page. With a mouse the canvas keeps the wheel by default, so write data-drag-canvas-wheel="0" on a canvas that sits in the middle of a long page.
Yes. A press that travels less than six pixels opens the card's link, and a drag never opens one. The arrow keys move the wall by one card, Tab reaches each card and brings it to the middle, and the copies are hidden from a screen reader, which reads each card once.