Print table
A gallery that starts as a pile. The photographs lie on a dark table the way a hand let them go: in small handfuls, each at its own angle, one over the other, a couple slid half out of the pile, none fully hidden, with a soft contact shadow.
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 grid belongs to the page: its columns, rows, gaps and spans are yours to write on the table, and the tidy state is that layout to the pixel. Three things the stylesheet cannot do for you: pinned, the grid has to fit one screen, so size its rows from the height left or setdata-print-table-hold="off"; no parent of the section may cut its overflow withoverflow: hidden, which stops the block from pinning, so useoverflow-x: clip; and a print must not clip its own overflow, or it loses its shadow. -
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-print-table Marks the section the page scrolls through while the prints are tidied. It takes no value: leave the value field empty. That section is what the stylesheet makes taller by the scroll the tidying takes, and what carries the settings. Without it on any element of the page, the embed marks the section holding the table. data-print-table-pin Marks the block that stays on screen, a child of the section, holding the table and whatever stays with it. It takes no value. Without it the engine takes the child of the section that holds the table. [ Optional ] data-print-table-surface Marks the table: the grid the prints are squared up into, and the frame they are kept in while loose. It takes no value. Its columns, gaps and spans are the page's own. Without it the engine takes the parent of the first marked print, and with neither mark nothing is mounted and the console says so. [ Optional ] data-print-table-print Marks one print, a direct child of the table. It takes no value. A print can be a link: a plain click still opens it, a drag never does. Without it on any of them, the engine takes the table's own children as the prints. [ Optional ] data-print-table-state Written by the engine on the section: pin while the block holds the screen, flow when hold is off, still under reduced motion. Never written by hand; it is what the stylesheet reads to pin the block and to give the prints their shadows. [ Optional ] data-print-table-tidy Written by the engine on the section while the grid is done, and taken off when a print leaves its cell. Never written by hand. [ Optional ] data-print-table-grab Written by the engine on the section when prints can be taken. Never written by hand; the stylesheet reads it for the open hand. [ Optional ] data-print-table-dragging Written by the engine on the section while a print is held. Never written by hand; the stylesheet reads it for the closed hand. [ Optional ] data-print-table-held Written by the engine on the print in the hand. Never written by hand; a rule of your own can read it. [ 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-print-table-shadow #000000 Colour of the shadow a print casts, on the table and on the prints under it. Options: any CSS colour. Black on a dark table; on a light table a dark tone of the table's own colour reads softer than black. data-print-table-scatter 0.7 How far apart the prints are thrown, from 0 to 1. data-print-table-seed 21 Which throw it is, a whole number. The same number on the same table gives the same pile, on every load. Options: any whole number, each one another pile. Try a few and keep the one that suits the pictures. data-print-table-rotation 16 Widest angle a thrown print lies at, in degrees. data-print-table-length clamp(900px, 170vh, 2400px) Scroll the tidying takes, any CSS length: how long the block holds the screen. Only read when hold is on. data-print-table-hold on Whether the block holds the screen while the prints are tidied: on or off. Options: on pins the block for length of scroll, right for a grid that fits one screen; off leaves the section in the flow and tidies the prints while the table rises through the window, right for a grid taller than the screen. data-print-table-start 0.1 Point of the scroll where the tidying starts, as a share from 0 to 1. Pinned, a share of length; not pinned, a share of the way the top of the table makes from the foot of the window to its top. data-print-table-end 0.8 Point of the same scroll where the grid is done, from 0 to 1. data-print-table-stagger 0.5 Head start of the first print on the last, from 0 to 1. data-print-table-overshoot 0.04 How far a print slides past its cell before settling, as a share of its way, from 0 to 0.3. Its angle swings past square by twice that. data-print-table-smooth 0.2 Time the prints take to catch the scroll, in seconds. data-print-table-drag on Whether a print can be picked up and thrown: on or off. Options: on lets the mouse, a pen or a finger take a print; off leaves the scroll as the only hand, and the prints plain links. data-print-table-friction 0.07 Share of its speed a thrown print loses at each frame, from 0 to 1. data-print-table-lift 1.06 Scale of a print held in the hand, 1 being its size on the table.
Two files and their pictures (14), 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
PrintTable.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-print-table-shadow #000000 Colour of the shadow a print casts, on the table and on the prints under it. Options: any CSS colour. Black on a dark table; on a light table a dark tone of the table's own colour reads softer than black. data-print-table-scatter 0.7 How far apart the prints are thrown, from 0 to 1. A tight stack in the middle of the table, spread only as far as it takes for every print to show A pile that covers more of the table, with more of each print in sight and the strays further out data-print-table-seed 21 Which throw it is, a whole number. The same number on the same table gives the same pile, on every load. Options: any whole number, each one another pile. Try a few and keep the one that suits the pictures. data-print-table-rotation 16 Widest angle a thrown print lies at, in degrees. Prints nearly square to the table, a neat pile; at 0 none is turned A messier throw. Past 30 the prints start to read as falling rather than lying data-print-table-length clamp(900px, 170vh, 2400px) Scroll the tidying takes, any CSS length: how long the block holds the screen. Only read when hold is on. A quick sweep, the prints snap into the grid A slow tidying the visitor can stop halfway, and more wheel to turn before the page goes on data-print-table-hold on Whether the block holds the screen while the prints are tidied: on or off. Options: on pins the block for length of scroll, right for a grid that fits one screen; off leaves the section in the flow and tidies the prints while the table rises through the window, right for a grid taller than the screen. data-print-table-start 0.1 Point of the scroll where the tidying starts, as a share from 0 to 1. Pinned, a share of length; not pinned, a share of the way the top of the table makes from the foot of the window to its top. The prints start moving at once A longer rest on the loose pile first, which is also the time a visitor has to play with it data-print-table-end 0.8 Point of the same scroll where the grid is done, from 0 to 1. A fast tidying, then a long rest on the finished grid The grid is done only just before the page moves on data-print-table-stagger 0.5 Head start of the first print on the last, from 0 to 1. At 0 every print moves at once, as one sheet The prints leave the pile one after the other, each crossing faster data-print-table-overshoot 0.04 How far a print slides past its cell before settling, as a share of its way, from 0 to 0.3. Its angle swings past square by twice that. At 0 the prints stop dead in their cells A springier, looser landing; past 0.1 it reads as a bounce data-print-table-smooth 0.2 Time the prints take to catch the scroll, in seconds. At 0 the prints are locked to the scroll, right with a smooth scroll library already on the page Prints that glide on after a wheel notch, and lag behind a fast scroll data-print-table-drag on Whether a print can be picked up and thrown: on or off. Options: on lets the mouse, a pen or a finger take a print; off leaves the scroll as the only hand, and the prints plain links. data-print-table-friction 0.07 Share of its speed a thrown print loses at each frame, from 0 to 1. A slippery table: the print slides far, often to the edge A rough table: the print stops a short way from the hand data-print-table-lift 1.06 Scale of a print held in the hand, 1 being its size on the table. At 1 the print does not grow, only its shadow says it is held A print held closer to the eye, further off the table -
Drive it from JavaScript
When a button has to throw the pile again, when the page reacts to a print being taken or dropped, or when the prints change after load.const element = document.querySelector('[data-print-table]'); element.addEventListener('printtable:tidy', (e) => { console.log(e.detail); }); const table = PrintTable.mount(element); // the section auto() mounted, or a new one table.scatter(12); // throws the whole pile again, with another seed table.progress(); // 0 the loose pile, 1 the grid table.places(); // where each print lies: { x, y, angle, moved } element.addEventListener('printtable:drop', (e) => { console.log(e.detail.index, e.detail.kept); // which print, and whether it stays there }); // When the section leaves the page, or before its prints change: PrintTable.destroy(element); // takes off every transform and gives back the grid as it was
-
Fit it to your page
- Structure: one section marked
data-print-table, holding one block markeddata-print-table-pin, the block that stays on screen. In it, the grid markeddata-print-table-surface: the table. Its direct children markeddata-print-table-printare the prints. Whatever else the block holds, a title, a line of words, stays with it. - The grid is yours. The table is a plain CSS grid, or any layout: columns, gaps, spans, ratios, breakpoints. The engine never sets a size. It reads where the layout put each print and animates from the pile to there, so the tidy state is exactly your layout, and a print in its cell carries no inline style at all.
- A print is anything: a link around a picture, a figure, a card with a caption. In the reference markup it is a link, a sheet of cream paper with the photograph and a frame number. Give each picture its
widthandheight, or give the cells a size of their own, so the grid does not move while the pictures load. - Parts without their mark: without
-surface, the parent of the first marked print is the table; without any-print, the children of the table are the prints; without-pin, the child of the section that holds the table is the block that stays on screen. - Pinned or not. With
holdon, the grid has to fit the block, one screen tall: that is the preview. For a grid taller than the screen, setdata-print-table-hold="off": the section stays in the flow, the pile is thrown over the whole table and tidied while the table rises through the window. - What the hand can do. A print is taken by pressing and moving: it rises, follows the hand with a short lag, leans with its speed when held off its middle, and comes to the top of the pile. Let go, it slides and stops, and the whole print stays on the table. A press on a print still sliding stops it.
- Where a moved print lives. While the pile is loose, a print stays where it stopped, and the scroll then takes it to its cell from there: scrolling back brings it to that new place, not to where it was first thrown. Once a print is more than halfway to its cell, the grid has it: a print pulled out then goes back to the place the scroll gives it, and one pulled out of the finished grid springs back to its cell. The grid always wins when tidy.
- A click is still a click. A press that travels less than six pixels opens the print's link. A drag never opens one.
- One setting for every table of the page, without opening the file: declare
window.PrintTableSettings = { scatter: 0.4 }in a script placed beforeprint-table.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-print-table-*attribute on the section itself (data-print-table-seed="12"), so one section can still differ from the page-wide setting. - What the script writes. On the section:
data-print-table-state,pinorflowwhile the pile is live andstillunder reduced motion;data-print-table-tidywhile the grid is done;data-print-table-grabwhen prints can be taken anddata-print-table-draggingwhile one is held. On a print: atransform, az-indexand two custom properties,--print-table-looseand--print-table-lift, from 0 to 1, which the two shadows read and any rule of your own can;data-print-table-heldon the print in the hand. All of it is taken off a print once it is in its cell. - From JavaScript:
PrintTable.mount(element, overrides)returns the instance, withelement,settings,prints,state(),progress()from 0 to 1,places()(where each print lies loose,{ x, y, angle, moved }),scatter(seed)which throws the whole pile again,refresh()after an attribute or the prints' sizes changed, anddestroy(). The section emitsprinttable:liftwith{ element, print, index }when a print is taken,printtable:dropwith{ element, print, index, x, y, angle, kept }when it has stopped or is sent back, andprinttable:tidywith{ element, tidy }when the grid is done and when it is left; the three bubble. - **
destroy()leaves the section as it was**: the listeners and the observers gone, the states, the custom properties and every transform taken off, the grid as the page wrote it. - Keyboard: the prints keep the Tab order of the markup, and a print that takes the focus comes to the top of the pile, where its ring shows. Enter opens its link. Nothing is dragged from the keyboard.
- Touch: a swipe up or down on the table scrolls the page, prints or not, so the pinned table never traps a phone. A finger that rests on a print for a moment picks it up, and so does one that moves sideways; from then on the page stays still and the print follows the finger. A tap opens the link.
- **
prefers-reduced-motion**: the grid, and nothing else. No pile, no pin, no drag; the links work as links. Turning the preference off throws the pile without a reload. - Without the script, the page is the same grid.
- Smooth scroll libraries: nothing to wire. The engine listens to scroll events and reads the section's place from the layout, which is what Lenis and the like move. Set
smoothto 0 if the two glides add up. - Out of the viewport the table sleeps. No frame is asked for unless the scroll moves near the section, a print is in the hand or one is still sliding.
- The words and the pictures of the markup are the preview's, and they ship as they are. Put the user's own 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 section marked
-
Avoid the pitfalls
- Pinned, the grid has to fit one screen. The block is exactly one window tall while it holds the screen, and a grid taller than that runs out of its foot. Size the rows from the height left in the block, as the preview does with
minmax(0, 1fr)rows, or setholdtooff. - An ancestor that cuts its overflow kills the pin, silently.
overflow: hiddenon a parent makes it the scroll container of the sticky block, and a container that does not scroll has nowhere to pin. Useoverflow-x: clipwhere a parent has to cut. - Give the section no height of its own. Pinned, its height is one screen plus
length, the second added by the stylesheet as the::afterof the section: a fixed height cuts the hold short, and a section that already uses its::afterloses it. - A print that clips its overflow has no shadow. The two shadows are the
::beforeand::afterof the print, painted around its box. Clip the picture inside the print, not the print itself, and leave both pseudo-elements to the component. - Leave the transform, the transition and the z-index of a print to the engine. It writes one transform per print from its cell. A CSS transition on
transformwould fight every frame of it; style what is inside the print. - The prints are direct children of the table. A wrapper between the two is taken as the print, and what is inside it never moves on its own. In a Webflow collection, the table is the Collection List and a print is the Collection Item.
- A grid that moves after load throws the pile again. The pile is thrown for the size of the table and of the prints. Pictures without
widthandheightin cells sized by their content change those sizes as they arrive, and each change is a new throw. - **The page-wide block only reaches sections mounted by
auto().**mount()on a section that already carries the effect returns the existing one. A later change goes through an attribute andrefresh(). - In the Webflow Designer the canvas shows the grid, since no Embed runs there: that is where the prints are edited. The pile appears in Preview and on the published site.
- Pinned, the grid has to fit one screen. The block is exactly one window tall while it holds the screen, and a grid taller than that runs out of its foot. Size the rows from the height left in the block, as the preview does with
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
Print table is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called scattered images to grid on scroll, scatter to grid gallery or photo pile gallery.
A gallery that starts as a pile. The photographs lie on a dark table the way a hand let them go: in small handfuls, each at its own angle, one over the other, a couple slid half out of the pile, none fully hidden, with a soft contact shadow. The section holds the screen while the page scrolls, and the scroll tidies them: each print slides to its cell on its own timing, the bottom of the pile first, along a slightly bent path, turns straight, runs a hair past its place and settles, like a hand squaring a stack. Scrolling back throws them where they were. Any print can be picked up, with the mouse, a pen or a finger: it rises, its shadow lengthens, it trails behind the hand and leans with its speed, comes to the top of the pile, and when thrown slides with friction and stops where it stops, kept on the table. A press that does not travel is a click, so a print stays a link. The grid is not the component's: it is a real CSS grid the page styles, columns, gaps and spans, and the engine reads the cells where the layout put them and animates between the pile and them with transforms only, so the tidy state is that layout to the pixel at every breakpoint, with no inline style left on it. Without the script, and under reduced motion, the page is the grid. No library: the hold is sticky positioning, the pile is seeded so it is the same on every load and thrown again for the size of the screen. Pattern type scattered gallery, photo pile to grid, contact sheet.
Updated
Questions
Mark a section data-print-table, the block that stays on screen data-print-table-pin, your grid data-print-table-surface and each of its direct children data-print-table-print. The grid stays a plain CSS grid of your own, columns, gaps and spans: the engine reads where the layout put each print and animates from the pile to there with transforms only. data-print-table-length is the scroll the tidying takes.
None of them: no library is loaded. The hold is sticky positioning, each print eases to its cell on its own timing and runs a hair past it, data-print-table-overshoot, and a thrown print slows with a friction the engine computes. In its cell a print carries no inline style at all.
Yes, the pile is seeded: data-print-table-seed picks the throw, and the same number gives the same pile. data-print-table-scatter, 0.7 by default, is how far apart the prints are thrown, and data-print-table-rotation, 16 degrees by default, the widest angle a print lies at. No print is ever fully hidden under the others.
Yes to both. A print is picked up by pressing and moving: it rises, trails behind the hand and leans with its speed, then slides and stops where it was thrown, kept on the table. A press that travels less than six pixels opens the print's link and a drag never does. data-print-table-drag="off" turns the hand off.
Set data-print-table-hold="off". The section then stays in the flow instead of holding the screen: the pile is thrown over the whole table and tidied while the table rises through the window. Without the script, and under reduced motion, the page is simply the grid.