Gravity footer
Small objects drop into a footer one at a time, stack up wherever they land against its floor and its two sides, and can be picked up and thrown with a mouse or a finger.
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 tutorialTwo things belong to the page rather than to the component. The objects need somewhere to gather: they stack against the bottom of the footer, so a footer whose content runs all the way down leaves them nowhere to be seen except behind the text. Keep a band of padding at the bottom, as the paste does, and the heap has a floor of its own. And the objects take their colours from the page:--gravity-footer-accentfirst, then the page’s own--accent, then the ink #15100c, with--gravity-footer-paneland--gravity-footer-inkdoing the same for their body and their marks. They are read when the pile is built, so a variable that lands later is not seen until the next play. -
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-gravity-footer Goes on the footer section and takes no value. It marks the element the objects fall into: they stack up against its floor and its two sides, and the footer’s own content stays in front of them, untouched. data-gravity-footer-state Written by the engine on the footer while the objects are coming down, and taken off once the heap is at rest. Never written by hand, and a page can style on it. [ Optional ] data-gravity-footer-pile Written by the engine on the footer while the effect is mounted, and removed by destroy(). The stylesheet makes the footer a stacking context of its own on it, and that is what makes the objects visible at all: without it the footer’s own background paints over every one of them, while the pile runs, sleeps and reports itself perfectly. Never written by hand. [ Optional ] data-gravity-footer-canvas Written by the engine on the canvas it lays inside the footer, and removed with it. The stylesheet fills the footer with that canvas and paints it over the footer’s background but behind every one of its blocks, so a link is never covered by an object. It takes no pointer event, so a click always reaches what is under it. Never written by hand. [ Optional ] data-gravity-footer-grab Written by the engine on the footer while objects can be picked up, so the stylesheet can hand every vertical gesture back to the browser with touch-action. It is the mark, not the grab setting below, and the two are spelled apart on purpose: a mark carries no value and a setting is read by its value. Never written by hand. [ Optional ] data-gravity-footer-debug-pieces, data-gravity-footer-debug-seconds, data-gravity-footer-debug-trigger Written by the engine on the footer at the end of a run, and only while the debug setting below is on: how many objects fell, how long the run took, and what set it off. They are there to read a preview that cannot be inspected. 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-gravity-footer-fill 0.11 How much of the footer's area the objects cover altogether, which is what sets how many there are. data-gravity-footer-size 0.11 How wide one object is, as a share of the footer's shorter side. data-gravity-footer-pull 1100 What pulls a falling object down, in pixels per second squared. This is the speed of the whole effect: it decides how long an object is in the air and so whether the fall can be read at all. data-gravity-footer-spawn 0.12 The time between two objects arriving, in seconds. A little unevenness is added to it, so a row of arrivals does not read as a machine pouring things out. data-gravity-footer-rebound 0.42 How much of its speed an object keeps off a wall or off another object. data-gravity-footer-grip 0.2 How much two objects and the floor hold on to each other. data-gravity-footer-grab true Whether objects can be picked up and thrown. Options: false, they are only ever looked at, true. data-gravity-footer-look impact How an object arrives and how its landing is shown. Drawing only: the physics is identical under all four. Options: raw (nothing added, the bare simulation), arrive (it fades and grows into its first fall), impact (arrive, plus a brief squash against the side it hit and a glint on that edge), ripple (arrive, plus a short wave leaving the point it touched). data-gravity-footer-delay 0 The wait before the first object arrives, in seconds. data-gravity-footer-trigger 0.9 How far into the screen the footer has to come before it plays, as a share of the viewport height. data-gravity-footer-replay false Whether it plays again every time the footer comes back into view. Options: false (once), true (every entry). data-gravity-footer-debug false Whether the last run is written on the footer as data-gravity-footer-debug-* attributes: objects, seconds, trigger. Options: false, true. For reading a preview that cannot be inspected. Local timeThe clock in the third column is not part of the effect: a short script at the end of the Embed keeps it running. Select the clock (named Local time in the Navigator), open the Settings panel, and change its two custom attributes. The word Local time above it is ordinary text. To drop the clock, delete its column and the script at the end of the Embed.Custom attribute What it does data-footer-zone The time zone the clock shows, written like Europe/Paris or America/New_York. Empty, it shows the visitor's own time. data-footer-hours Opening hours, from and to, like 9-17. The order desk line and the dot say open or closed from them, Monday to Friday.
Two files, no build step. It also needs Matter.js on the page, loaded with the tag in step 1. Without the tag the footer is left exactly as it is and says so once in the console.
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
GravityFooter.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-gravity-footer-fill 0.11 How much of the footer's area the objects cover altogether, which is what sets how many there are. A handful, scattered along the floor A deep heap that climbs the footer data-gravity-footer-size 0.11 How wide one object is, as a share of the footer's shorter side. Many small marks A few big ones data-gravity-footer-pull 1100 What pulls a falling object down, in pixels per second squared. This is the speed of the whole effect: it decides how long an object is in the air and so whether the fall can be read at all. Slow and floaty, and past a point simply dull A short hard drop, and past a point objects are at the bottom before the eye can follow one data-gravity-footer-spawn 0.12 The time between two objects arriving, in seconds. A little unevenness is added to it, so a row of arrivals does not read as a machine pouring things out. They pour in, and it reads as a shower rather than as objects They arrive one by one, slowly data-gravity-footer-rebound 0.42 How much of its speed an object keeps off a wall or off another object. Dead weight, it stops where it lands Lively, it bounces around before settling data-gravity-footer-grip 0.2 How much two objects and the floor hold on to each other. Slippery, everything slides flat Grippy, objects roll and hold a slope data-gravity-footer-grab true Whether objects can be picked up and thrown. Options: false, they are only ever looked at, true. data-gravity-footer-look impact How an object arrives and how its landing is shown. Drawing only: the physics is identical under all four. Options: raw (nothing added, the bare simulation), arrive (it fades and grows into its first fall), impact (arrive, plus a brief squash against the side it hit and a glint on that edge), ripple (arrive, plus a short wave leaving the point it touched). data-gravity-footer-delay 0 The wait before the first object arrives, in seconds. It starts the moment the footer is reached It lets something else go first data-gravity-footer-trigger 0.9 How far into the screen the footer has to come before it plays, as a share of the viewport height. It waits until the footer has climbed higher up the screen It plays as soon as the footer shows at the bottom data-gravity-footer-replay false Whether it plays again every time the footer comes back into view. Options: false (once), true (every entry). data-gravity-footer-debug false Whether the last run is written on the footer as data-gravity-footer-debug-* attributes: objects, seconds, trigger. Options: false, true. For reading a preview that cannot be inspected. Local timeThe clock in the third column is not part of the effect: the script at the end of the markup keeps it running. Its two settings are attributes on the element carryingdata-footer-clock. To drop it, delete the column and that script.Custom attribute What it does data-footer-zone The time zone the clock shows, written like Europe/Paris or America/New_York. Empty, it shows the visitor's own time. data-footer-hours Opening hours, from and to, like 9-17. The order desk line and the dot say open or closed from them, Monday to Friday. -
Drive it from JavaScript
When the page reacts to the heap coming to rest, when the footer is built after the page has loaded, or when the pile has to be played again.const element = document.querySelector('[data-gravity-footer]'); element.addEventListener('gravityfooter:done', (e) => { console.log(e.detail); }); const footer = GravityFooter.mount(element); // the footer auto() mounted, or a new one footer.objects(); // the objects, { x, y, r, spin, asleep, held, onFloor }, read only // Play it again, whatever the trigger says. footer.play(); // A footer added after the page loaded: mount everything marked inside it. GravityFooter.auto(document.body); // When the footer leaves the page: GravityFooter.destroy(element); // ends the run and clears everything it wrote
-
Fit it to your page
- Mark the footer:
data-gravity-footer, no value, on the<footer>or on the section that holds it. - Settings go on the same element, as more attributes:
data-gravity-footer-fill="0.18",data-gravity-footer-grab="false". Three levels, least specific first:DEFAULTS, thenwindow.GravityFooterSettings = { fill: 0.2 }declared before the engine, then the footer's attributes. Settings are read again at every start. - **It plays when the footer reaches
trigger**, and a footer too close to the end of the page to reach that line plays once the page is scrolled as far as it goes, which for a footer is the usual case. - Picking an object up works with a mouse, a pen and a finger. With a finger, a sideways drag takes an object and a vertical gesture always scrolls the page: that is not a compromise made late, it is the rule the component is built on. A gesture the browser decides to keep arrives as a
pointercanceland simply drops the object. No wheel listener is registered anywhere, so the wheel is never touched either. A press that lands on a link, a button or a field is left to that element, always. - From JavaScript:
GravityFooter.mount(el, overrides)returns{ element, settings, play(), objects(), destroy() }.objects()gives the objects as{ x, y, r, spin, asleep, held, onFloor }in the footer's own coordinates, read only.GravityFooter.play(el)replays it,GravityFooter.destroy(el),GravityFooter.auto(root)andGravityFooter.defaultscomplete it. - Events, both bubbling:
gravityfooter:startandgravityfooter:donewith{ element, trigger, pieces },donealso carryingdurationin seconds;triggerisscroll,focus,api, orsafetywhen the safety net let the footer be without playing it.donefires once, when the heap first comes to rest; picking an object up afterwards restarts the simulation without any further event.destroy()is a quiet teardown: it ends a run without adone. - **Under
prefers-reduced-motion: reduce** nothing is ever seen moving. The same solver runs with nothing drawn until every object sleeps, and one single frame is painted, so the heap is real but was never an animation; objects cannot be picked up either, since a thrown object is an animation whatever started it. Both events still fire. - The footer stays a footer. The objects live on a canvas behind the content,
aria-hidden, never in the tab order. The links are real links, the text stays selectable, the reading and tab order never change, and the page does not shift by a pixel. A keyboard user who reaches the footer before it has been scrolled into view sets it off by focusing it. - 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.
- Mark the footer:
-
Avoid the pitfalls
- The stylesheet matters. Without
gravity-footer.cssthe canvas has no box to fill and no place behind the content. The engine dresses it inline when the sheet is missing, but the sheet is what makes it right. - The pile needs somewhere to gather. Its objects stack against the bottom of the footer, so a footer whose content runs all the way down leaves them nowhere to be seen except behind the text. Give it a band of padding at the bottom, as the demo does, and the heap has a floor of its own.
- The objects take their colours from the page:
--gravity-footer-accent, then the page's--accent, then the amber#ffab1a;--gravity-footer-paneland--gravity-footer-inkdo the same for their body and their marks. They are read when the pile is built, so a variable that lands later is not seen until the next play. - **Do not put
touch-actionof your own on the footer.** The component setspan-y pinch-zoom, and that is the whole reason a vertical gesture still scrolls the page.touch-action: nonethere would take the reader's scroll. - In Webflow, the mark and the settings are all read in the Settings panel, on real elements. Nothing depends on a class, so restyling the footer cannot break the effect.
- The stylesheet matters. Without
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
Gravity footer is an animated navigation component for Webflow and vanilla JavaScript, built on Matter.js 0.20.0. Also called falling objects footer or physics footer.
Small objects drop into a footer one at a time, stack up wherever they land against its floor and its two sides, and can be picked up and thrown with a mouse or a finger. The footer's own links sit in front of them, readable and clickable, and the page never loses its scroll. It stops dead when everything is still: no frame is drawn at rest. No build step and no image file; the objects are drawn with canvas calls alone. Nothing is measured or written at rest, and with no JavaScript, with reduced motion, or if the library never arrives, the footer is simply there.
Updated
Questions
Select the footer, open the Settings panel, and under Custom attributes add data-gravity-footer with an empty value. Settings go on that same element as more attributes, for instance data-gravity-footer-fill="0.18". Nothing depends on a class, so restyling the footer cannot break the effect.
Matter.js 0.20.0, under the MIT licence, loaded from one pinned CDN script tag. Matter's own renderer and its MouseConstraint are not used: the objects are drawn on one canvas and the drag is written on pointer events. Without the tag the footer is left as it is and says so once in the console.
There is no object count: set data-gravity-footer-fill, the share of the footer's area the objects cover, 0.11 by default. Lower gives a handful scattered along the floor, higher a deep heap that climbs the footer. data-gravity-footer-size, also 0.11, is the width of one object as a share of the footer's shorter side.
Yes. The objects live on a canvas behind the content, marked aria-hidden and never in the tab order, and a press that lands on a link, a button or a field is always left to that element. The text stays selectable and the reading order never changes.
No. With a finger, a sideways drag picks an object up and a vertical gesture always scrolls the page, because the component sets touch-action: pan-y pinch-zoom and registers no wheel listener. Do not put a touch-action of your own on the footer, and write data-gravity-footer-grab="false" if the objects should only be looked at.