Leaflet
A section that arrives folded, cover up, and that the scroll opens as a hand would.
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 tutorialThree things belong to the page rather than to the component: no parent of the section may cut its overflow withoverflow: hidden, which stops the block from pinning, so useoverflow-x: clipwhere a parent has to cut; the sheet itself takes no background, overflow, filter or opacity, which would flatten the fold; and the panels sit in one row or one column, never a grid of both. -
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-leaflet Marks the section the page scrolls through while the leaflet opens. It takes no value: leave the value field empty. That section is what the stylesheet makes taller by the scroll the unfolding takes, and what carries the settings. Without it on any element of the page, the embed marks the block holding the screen. data-leaflet-pin Marks the block that stays on screen, a child of the section: it holds the sheet, and gives the panels their perspective. It takes no value. Without it the engine takes the first block of the section. [ Optional ] data-leaflet-sheet Marks the sheet, the grid or flex row whose children are the panels. It takes no value, and no background, overflow, filter or opacity of its own. Without it the engine takes the last block of the block that stays on screen. [ Optional ] data-leaflet-panel Marks a panel, a child of the sheet: two to six of them, in one row or one column, each with any content. It takes no value. Add one, take one out, and the fold follows. Without it on any child, every child of the sheet is a panel. [ Optional ] data-leaflet-cover Marks the cover, a child of the sheet: it is laid on the back that ends on top of the folded leaflet and only shows while it is folded. It takes no value. Left out when an accordion of three or five panels ends face up; without the script and under reduced motion it is an ordinary child of the sheet. [ Optional ] data-leaflet-state Written by the engine on the section: pinned when the open sheet fits the screen, flowing when it unfolds in the flow of the page, still under reduced motion. Never written by hand; it is what the stylesheet reads to pin the block and give it depth. [ Optional ] data-leaflet-axis Written by the engine on the section: x when the panels sit side by side, y when they sit one under the other. Never written by hand; the paint of the creases turns with it. [ Optional ] data-leaflet-open Written by the engine on the section while the sheet lies flat. 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-leaflet-paper #f4ece0 Colour of the face of the sheet. Left at its default, each panel keeps the background its own class gives it; changed, every panel is painted with it. Options: any CSS colour. To colour one panel only, leave this alone and give that panel a background in its own class. data-leaflet-back #e9dfce Colour of the back of the sheet, the side that shows while a panel turns. Options: any CSS colour. A shade under paper reads as the unprinted side; the same colour reads as a sheet printed on both sides. data-leaflet-shadow #050302 Colour of every shadow: on the surface, in the folds, and on a face turned away from the light. Options: any CSS colour. A dark shade of the surface colour sits better than pure black. data-leaflet-fold accordion How the sheet is folded: accordion, roll or gate. Options: accordion folds one way then the other, and opens two panels at a time, as a tent that lies down; roll wraps every panel the same way and unrolls them one by one; gate closes the two ends on the middle like doors and opens them together. A gate needs three panels at least. data-leaflet-opens forward End the sheet opens towards: forward or backward. A gate opens from its middle and ignores it. Options: forward keeps the first panel down and opens towards the last, to the right or downwards; backward keeps the last panel down and opens towards the first. data-leaflet-length clamp(1500px, 320vh, 4400px) Scroll the unfolding takes while the sheet is pinned, any CSS length. Not read when the open sheet is taller than the screen and unfolds in the flow of the page. data-leaflet-smooth 0.18 Time the sheet takes to catch the scroll, in seconds. data-leaflet-overlap 0.3 Share of a fold played together with the next one, 0 to 1. data-leaflet-perspective clamp(1800px, 160vw, 3000px) Distance of the eye from the surface, any CSS length. data-leaflet-tilt 18 Lean of the folded sheet away from the eye, in degrees, while it is pinned. It comes back to 0 as the sheet lies flat. data-leaflet-lift clamp(14px, 2.2vw, 40px) Height the leaflet is held over the surface while it is handled, any CSS length. It lands as it lies flat. data-leaflet-thickness 1.5 Thickness of one layer of the folded stack, in px. It is the room each crease leaves for the layers it wraps. data-leaflet-light 0.5 Strength of the light on the panels, 0 to 1: how far a face brightens towards the light and darkens away from it. data-leaflet-depth 0.5 Strength of the shadows, 0 to 1: the one on the surface and the one a closing fold lays on a face. data-leaflet-crease 0.5 Mark the folds leave on the flat sheet, 0 to 1: a hairline on each folded edge and a shade in each valley. data-leaflet-grain 0.5 Grain of the paper, 0 to 1. data-leaflet-pool 0.5 Light on the surface around the sheet, 0 to 1. A shadow only shows on a surface that is lit.
Two files and their pictures (1), 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
Leaflet.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-leaflet-paper #f4ece0 Colour of the face of the sheet. Left at its default, each panel keeps the background its own class gives it; changed, every panel is painted with it. Options: any CSS colour. To colour one panel only, leave this alone and give that panel a background in its own class. data-leaflet-back #e9dfce Colour of the back of the sheet, the side that shows while a panel turns. Options: any CSS colour. A shade under paper reads as the unprinted side; the same colour reads as a sheet printed on both sides. data-leaflet-shadow #050302 Colour of every shadow: on the surface, in the folds, and on a face turned away from the light. Options: any CSS colour. A dark shade of the surface colour sits better than pure black. data-leaflet-fold accordion How the sheet is folded: accordion, roll or gate. Options: accordion folds one way then the other, and opens two panels at a time, as a tent that lies down; roll wraps every panel the same way and unrolls them one by one; gate closes the two ends on the middle like doors and opens them together. A gate needs three panels at least. data-leaflet-opens forward End the sheet opens towards: forward or backward. A gate opens from its middle and ignores it. Options: forward keeps the first panel down and opens towards the last, to the right or downwards; backward keeps the last panel down and opens towards the first. data-leaflet-length clamp(1500px, 320vh, 4400px) Scroll the unfolding takes while the sheet is pinned, any CSS length. Not read when the open sheet is taller than the screen and unfolds in the flow of the page. A quick opening, a flick of the wheel per fold A slow, detailed opening, and more wheel to turn before the page goes on. Around 100vh per fold reads well data-leaflet-smooth 0.18 Time the sheet takes to catch the scroll, in seconds. At 0 the sheet is locked to the scroll, right with a smooth scroll library already on the page A sheet that glides after a wheel notch, and lags behind a fast scroll data-leaflet-overlap 0.3 Share of a fold played together with the next one, 0 to 1. At 0 each fold ends before the next begins: clear steps, and a dead moment between them Folds that run into each other, one continuous gesture; near 1 they all open at once data-leaflet-perspective clamp(1800px, 160vw, 3000px) Distance of the eye from the surface, any CSS length. A close eye: panels loom large as they swing, with strong foreshortening A distant eye: a calmer, flatter move, closer to a technical drawing data-leaflet-tilt 18 Lean of the folded sheet away from the eye, in degrees, while it is pinned. It comes back to 0 as the sheet lies flat. At 0 the leaflet is seen from straight above The surface leans back like a desk seen from a chair, which shows the height of the folds. A negative value leans it the other way data-leaflet-lift clamp(14px, 2.2vw, 40px) Height the leaflet is held over the surface while it is handled, any CSS length. It lands as it lies flat. At 0 the sheet never leaves the surface and its shadow stays tight The sheet floats higher: a wider, softer shadow, further from it data-leaflet-thickness 1.5 Thickness of one layer of the folded stack, in px. It is the room each crease leaves for the layers it wraps. At 0 the layers of the stack share one plane and can flicker through each other A thicker stack, whose edges show as steps when it is folded data-leaflet-light 0.5 Strength of the light on the panels, 0 to 1: how far a face brightens towards the light and darkens away from it. At 0 every face keeps one flat colour whatever its angle Strong contrast between the two slopes of a fold data-leaflet-depth 0.5 Strength of the shadows, 0 to 1: the one on the surface and the one a closing fold lays on a face. At 0 no shadow at all, the sheet floats on nothing Dense shadows, a harder light data-leaflet-crease 0.5 Mark the folds leave on the flat sheet, 0 to 1: a hairline on each folded edge and a shade in each valley. At 0 the open sheet is one smooth surface A sheet that has clearly been folded, each crease drawn data-leaflet-grain 0.5 Grain of the paper, 0 to 1. At 0 the paper is a flat colour A rougher stock, visible fibres data-leaflet-pool 0.5 Light on the surface around the sheet, 0 to 1. A shadow only shows on a surface that is lit. At 0 the surface keeps its own flat colour, and on a dark one the shadow disappears A brighter pool of light under the sheet, and a shadow that reads from further away -
Drive it from JavaScript
When the panels or an attribute change after load, or when the page reacts to the sheet lying flat.const element = document.querySelector('[data-leaflet]'); element.addEventListener('leaflet:change', (e) => { console.log(e.detail); }); const leaflet = Leaflet.mount(element); // the section auto() mounted, or a new one leaflet.progress(); // 0 folded, 1 flat leaflet.isOpen(); // true once the sheet lies flat leaflet.refresh(); // after changing an attribute or the panels element.addEventListener('leaflet:open', () => { console.log('the sheet lies flat'); }); // When the section leaves the page: Leaflet.destroy(element); // removes the added layers and gives back the markup as it was
-
Fit it to your page
- Structure: one section marked
data-leaflet, holding one block markeddata-leaflet-pin, the block that stays on screen, and in it the sheet, markeddata-leaflet-sheet. The children of the sheet markeddata-leaflet-panelare the panels. Without the marks the engine takes the first block of the section, the last block of that block, and every child of the sheet. - Making it yours: the sheet is your own grid or flex row, and a panel is any block of yours, with any content: text, a list, a picture, links, a form. Put two to six of them in one row and style them as you like. The engine reads where the layout puts them and folds along the edges they share, so panels of different widths fold as they are. Change the number of panels and the fold follows.
- One row or one column, never a grid of both. Panels side by side fold across; panels one under the other fold downwards. That is read from the layout each time it changes, so a media query that stacks the panels on a phone is all it takes to turn the fold.
- Pinned, or in the flow. When the open sheet fits the screen, the block is held while the page scrolls through
length, and the leaflet opens in place. When it is taller than the screen, which is the case of stacked panels on a phone, nothing is pinned: the leaflet starts folded at the top of its place and each panel comes down as the page reaches it. - The cover is optional: a child of the sheet marked
data-leaflet-cover. It is laid on the back that ends on top of the folded leaflet, at the size of that panel, so it shows while the leaflet is folded and goes under as it opens. On a gate with an even count it takes the size of the two doors and is shared between them. An accordion of three or five panels ends face up, its last panel on top: that panel is its cover, and the cover block is left out. Under reduced motion and without the script the cover is an ordinary child of the sheet: the look lays it across the top. - Colours: the panels take their background from their own class. The
papersetting is there to repaint all of them at once from an attribute,data-leaflet-paper="#ffffff";backandshadowcolour what the engine adds. - One setting for every section of the page, without opening the file: declare
window.LeafletSettings = { fold: 'roll' }in a script placed beforeleaflet.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-leaflet-*attribute on the section itself (data-leaflet-fold="gate"), so one section can still differ from the page-wide setting. - What the script writes:
data-leaflet-stateon the section,pinned,flowingorstill;data-leaflet-axis,xory;data-leaflet-openonce the sheet lies flat. A transform on each panel and on the sheet while it moves, taken off when it lies flat. A few custom properties on each panel, the numbers its paint reads. And its own layers inside the sheet: one back per panel, and the shadow on the surface, all hidden from assistive technology. - Changing the paper: the paint of a face is one rule of
leaflet.css, on the::afterof a panel: a hairline on each folded edge, the shadow a closing fold lays, the light. The grain is its::before. The engine only writes numbers (--leaflet-lit,--leaflet-dim,--leaflet-near,--leaflet-far); the colours and the stops are yours to change. - From JavaScript:
Leaflet.mount(element, overrides)returns the instance, withelement,settings,state(),progress()from 0 folded to 1 flat,isOpen(),refresh()after an attribute or the panels changed, anddestroy(). The section emitsleaflet:changewith{ element, state },leaflet:openwhen the sheet comes to lie flat andleaflet:closewhen it leaves it; the three bubble. - **
destroy()leaves the section as it was**: the added layers removed, the transforms, the custom properties and the states taken off, the listeners and the observers gone. - Keyboard: every link of the sheet stays in the tab order. A link that takes the focus while its panel is folded brings the page to where the sheet lies flat, so what has the focus is in sight.
- Touch: the leaflet follows the page scroll, so a finger drives it as a wheel does. Nothing listens to touch events.
- **
prefers-reduced-motion**: no pin and no fold. The sheet is flat, with its creases and its shadow, and the cover across its top. Turning the preference off brings the fold back without a reload. - Without the script, the section is the flat sheet: the grid as it was written.
- 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. - The words and the picture 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
- An ancestor that cuts its overflow stops the pin, silently:
overflow: hiddenon a parent makes it the scroll container of the sticky block. Useoverflow-x: clipwhere a parent has to cut. The console says which ancestor it is. - Nothing on the sheet may flatten it. The sheet is the 3D space the panels turn in: an
overflow, afilter, anopacityunder 1, aclip-pathor amix-blend-modeon the block markeddata-leaflet-sheetflattens it, and the panels then slide over each other like cards. The same properties are fine on a panel or inside it. - The sheet itself has no background. The paper is the panels: a colour on the sheet would stay on the surface as a flat rectangle while the panels fold above it.
- A gap between two panels is kept as paper that is not there: the fold still works, with the surface showing through the crease. For one continuous sheet, no
gap: give the panels padding instead. - Both pseudo-elements of a panel are taken:
::beforeis the grain,::afterthe light. A decoration of your own goes on an element inside the panel. - The engine adds layers inside the sheet: the shadow as its first child, the backs after the last panel. They are positioned and take no place in a grid, but a rule written with
:first-childor:last-childon the panels would miss; use:first-of-typeon a class, or a class. - Wrapping panels are not a fold. A grid of two rows of panels has no single line to fold along: keep one row, or one column.
- A page too short to scroll leaves the leaflet folded. In the flow of the page, a sheet opens over the scroll it takes to bring its last panel into the window; pinned, over
length. Both need the page to scroll. - Settings are read at mount: after changing an attribute, or adding or removing a panel, call
Leaflet.refresh(element).
- An ancestor that cuts its overflow stops the pin, silently:
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
Leaflet is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called paper fold animation on scroll, 3D fold scroll animation or unfolding brochure effect.
A section that arrives folded, cover up, and that the scroll opens as a hand would. The sheet is the page's own grid: two to six panels in one row, each with any content. The engine measures them where the CSS puts them and folds the row along the edges the panels share, as paper folds: an accordion whose panels swing over two by two and stand as a tent before they lie down, a roll that unrolls one panel at a time, or a gate whose two doors open together. Every panel is hinged on its crease in real perspective, the stack has a thickness so no panel passes through another, and each panel has a back made of paper that shows while it turns. The light is one fixed direction: a face turned towards it brightens, a face turned away darkens, a closing fold lays its shadow from the crease, and the whole sheet casts a soft shadow on the surface under it, projected along the same light. A cover block is printed on the back that ends on top of the folded leaflet; on a gate it is shared by the two doors. Once flat, the transforms come off: the section is the grid it was written as, the text selects, the links work, and the creases stay faintly marked, as on paper that has been folded. Panels laid one under the other, the layout of a phone, fold and unfold downwards, in the flow of the page instead of pinned. Eased towards the scroll and reversible. No library: the hold is sticky positioning and the place is read from the layout, so it follows the browser's own scroll and a smooth scroll library alike. Under reduced motion and without the script, the flat sheet. Pattern type scroll unfold, 3D fold, paper.
Updated
Questions
Mark a section data-leaflet, the block that stays on screen data-leaflet-pin, the row inside it data-leaflet-sheet and each of its two to six children data-leaflet-panel. The sheet is your own grid or flex row: the engine reads where the layout puts the panels and folds along the edges they share. data-leaflet-length is the scroll the unfolding takes.
No library at all. The hold is CSS sticky positioning, the place in the scroll is read from the layout, and each panel is hinged on its crease by a matrix3d in real perspective. It follows the browser's own scroll and a smooth scroll library alike.
Three, set with data-leaflet-fold. accordion, the default, swings the panels over two by two and stands them as a tent before they lie down; roll unrolls one panel at a time; gate opens two doors together. data-leaflet-opens picks the end the sheet opens towards, and data-leaflet-overlap, 0.3 by default, how much of a fold plays with the next one.
Yes. Once the sheet lies flat the transforms come off: the section is the grid it was written as, the text selects and the links work, with the creases left faintly marked. data-leaflet-crease sets that mark, and at 0 nothing shows the sheet was ever folded.
Panels laid one under the other fold and unfold downwards: the direction is read from the layout, so a media query that stacks the panels is all it takes. A sheet taller than the screen is not pinned, it starts folded at the top of its place and each panel comes down as the page reaches it. Under reduced motion and without the script the page shows the flat sheet.