Card fan
A deck of cards, each one a link: projects, products, articles, samples.
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 stylesheet stacks the cards of the deck in one cell, so the deck is as tall as one card: the open fan spills over it, above by the lift and below by the foot of the arc. Give the deck that room, padding or margin, and keepoverflow: visibleon it and on its parents, or the fan is clipped. The deck's width is the room the fan may take. -
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-card-fan Marks the deck, the box that holds the cards. It takes no value: leave the value field empty. That box is what the effect is mounted on, what carries the settings, and its width is the room the open fan may take. Without it on any box of the page, the embed marks the parent of the first card it finds next to itself, and says so in the console when there is none. data-card-fan-card Marks one card, a direct child of the deck. It takes no value. A card is what lifts under the pointer and what takes the focus, so in the preview it is the link itself. Without it on any card, nothing is mounted and the console says so. [ Required ] data-card-fan-state Written by the engine: idle, open, closing or still on the deck, active on the lifted card. Never written by hand; the stylesheet reads it for the pile and for the strip under the lifted card. [ 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-card-fan-shadow #15100c Tint of the shadow under each card and of its hairline, the ink in the cream and ink look. Options: any CSS colour, transparent for no shadow at all; taken out of this block, --card-fan-shadow on the deck takes over. data-card-fan-spread 46 Angle between the first card and the last one of the open fan, in degrees. data-card-fan-radius clamp(420px, 56vw, 900px) Radius of the arc the cards sit on, any CSS unit: how far below the cards the hand that holds them is. Shortened by the script when the deck is too narrow for the fan. data-card-fan-lift clamp(22px, 3.2vw, 52px) How far the card under the pointer rises out of the arc, any CSS unit. data-card-fan-room clamp(12px, 1.8vw, 30px) How far its two neighbours slide along the arc to make room, any CSS unit; each next card moves half as far. data-card-fan-grow 1.06 Size of the lifted card, in times its own size. data-card-fan-stagger 0.035 Delay between two cards as the fan opens and as it folds, in seconds. data-card-fan-duration 0.55 Time a card takes to settle after it is sent somewhere, in seconds. data-card-fan-bounce 0.22 Overshoot of the spring, 0 to 1. data-card-fan-peek 3 How many cards show in the pile at rest, counted from the top one; the others sit hidden right under the last one that shows. data-card-fan-tilt 5 Largest lean of a card in the pile, in degrees; each card that shows takes its own share of it.
Two files and their pictures (6), 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
CardFan.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-card-fan-shadow #15100c Tint of the shadow under each card and of its hairline, the ink in the cream and ink look. Options: any CSS colour, transparent for no shadow at all; taken out of this block, --card-fan-shadow on the deck takes over. data-card-fan-spread 46 Angle between the first card and the last one of the open fan, in degrees. A flat row of barely leaning cards; at 0 the cards only slide apart A wide hand; past 90 the end cards lie on their side data-card-fan-radius clamp(420px, 56vw, 900px) Radius of the arc the cards sit on, any CSS unit: how far below the cards the hand that holds them is. Shortened by the script when the deck is too narrow for the fan. A tight hand, the cards close together and the arc strongly curved A wide, shallow arc, the cards far apart data-card-fan-lift clamp(22px, 3.2vw, 52px) How far the card under the pointer rises out of the arc, any CSS unit. At 0 the card only straightens and grows A card that leaves the hand altogether data-card-fan-room clamp(12px, 1.8vw, 30px) How far its two neighbours slide along the arc to make room, any CSS unit; each next card moves half as far. At 0 the neighbours stay put The fan visibly splits open around the lifted card data-card-fan-grow 1.06 Size of the lifted card, in times its own size. At 1 the card keeps its size; under 1 it shrinks Past 1.15 the card reads as zoomed, and soft if its picture is small data-card-fan-stagger 0.035 Delay between two cards as the fan opens and as it folds, in seconds. At 0 every card leaves at once Past 0.08 the fan opens like cards being dealt one by one data-card-fan-duration 0.55 Time a card takes to settle after it is sent somewhere, in seconds. A snappy fan, nervous under 0.3 A slow, heavy fan data-card-fan-bounce 0.22 Overshoot of the spring, 0 to 1. At 0 a card arrives without passing its place Past 0.4 the cards wobble before they settle data-card-fan-peek 3 How many cards show in the pile at rest, counted from the top one; the others sit hidden right under the last one that shows. At 1 a single leaning card Every card of the deck leaning its own way data-card-fan-tilt 5 Largest lean of a card in the pile, in degrees; each card that shows takes its own share of it. At 0 a square pile where only the top card shows A messy heap -
Drive it from JavaScript
When the cards change after load, when something else than the pointer has to open the fan, or when the page reacts to the card that is lifted.const element = document.querySelector('[data-card-fan]'); element.addEventListener('cardfan:change', (e) => { console.log(e.detail); }); const fan = CardFan.mount(element); // the deck auto() mounted, or a new one fan.open(); // fans the pile out and holds it open fan.select(2); // lifts the third card fan.close(); // folds the fan back element.addEventListener('cardfan:change', (e) => { console.log('lifted card', e.detail.index); }); // When the deck leaves the page, or before its cards are rendered again: CardFan.destroy(element); // clears every state, property and transform
-
Fit it to your page
- Structure: a box marked
data-card-fan, the deck, and inside it, as direct children, one element per card markeddata-card-fan-card. In the reference markup a card is a link holding one picture, so the whole card is clickable and takes the focus. A card can hold anything: a picture, or a title and a few lines on a background. - Order: the last card of the markup is the top of the pile, and the right end of the fan. The first card is the left end.
- The size of a card is a class, not a setting. A card is a real box: its width, its ratio and its background are the page's. Every card of a deck has the same size.
- The deck needs room around the pile. The cards are stacked in one cell, so the deck is as tall as one card and the open fan spills over it: above by
lift, below by the foot of the arc. The preview gives the deck a padding for that. The deck's own width is the room the fan may take: when the fan set byradiusdoes not fit, the radius is shortened until it does. - One setting for every deck of the page, without opening the file: declare
window.CardFanSettings = { spread: 60 }in a script placed beforecard-fan.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-card-fan-*attribute on the deck itself (data-card-fan-spread="60"), so one deck can still differ from the page-wide setting. - The tint can also come from the stylesheet: without a
shadowsetting in the attributes or in the settings block, the custom property--card-fan-shadowon the deck is what the shadow reads. The engine writes its own copy ofDEFAULTSinto the Embed, so on a Webflow page the attribute is the way to change it per deck. - What the script writes.
data-card-fan-stateon the deck:idleat rest,open,closingwhile the fan folds,stillunder reduced motion; andactiveon the lifted card. On each card, three custom properties for its pose in the pile,--card-fan-rest-r,--card-fan-rest-xand--card-fan-rest-y, plain numbers the stylesheet turns into a transform, and atabindex. While the fan moves, an inlinetransform, az-indexand--card-fan-up, all taken off once the pile is back. - **From JavaScript, beyond
mount,autoanddestroy**: the api carrieselement,settings,cards,open()which fans the pile out and holds it,close(),select(index)which lifts a card,isOpen(),isRunning()andrefresh(), to call after a setting or an attribute changed. The deck emitscardfan:open,cardfan:change{ index, card }when the lifted card changes, andcardfan:close. All bubble. - **
destroy()leaves the deck as it was**: the listeners gone, the states and the custom properties taken off, no transform left inline, and every card back with thetabindexit had. - Keyboard: the deck is one Tab stop. Tab into it opens the fan and lifts the card that has the focus; the arrows move from card to card, Home and End go to the two ends, Enter follows the card, Escape folds the fan. Tab again leaves the deck and folds it.
- Touch: there is no hover, so each step takes a tap. A tap on the pile opens it, a tap on a card lifts it, a tap on the lifted card follows its link, and a tap anywhere else folds the fan. A finger sliding across the open fan runs through the cards.
- **
prefers-reduced-motion**: no pile and no fan. The stylesheet stops stacking the cards and they stay in the row the page lays them in; the script moves nothing, and the keyboard still walks the row. Turning the preference off brings the pile back without a reload. - The pictures 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.
- In the Webflow Designer the cards show in a row. The Designer never runs the Embed, so the canvas shows the deck as the class lays it, a row where every card can be selected and its picture swapped. The pile and the fan show in preview and on the published site.
- Structure: a box marked
-
Avoid the pitfalls
- The cards have to be direct children of the deck. The stylesheet puts them in the same grid cell with a child selector; a wrapper between the deck and a card takes the card out of the pile.
- Nothing around the deck may clip it.
overflow: hiddenon the deck, on the section or on a parent cuts the lifted card and the two ends of the fan. - **The deck's
displayis the stylesheet's while motion is allowed.** The pile is a one-cell grid, written with more weight than a plain class. Thedisplaya class of the page gives the deck is what shows under reduced motion, and before the stylesheet is there. - A card needs a size that does not depend on the deck. A card set to
width: 100%is as wide as the deck, and the fan then has no room to open: give the cards a width of their own. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a deck that already carries the effect returns the existing one. A page-wide block goes inwindow.CardFanSettings, whichauto()reads itself; after changing an attribute by script, callrefresh(). - **The shadow is a
color-mix()of the tint.** A browser too old forcolor-mix()drops the whole shadow, hairline included, and the cards then only differ by their own colours. - Cards added or removed after mount are not in the fan. Call
destroy()thenmount()once the deck has its new cards. - A transform of the page on a card is replaced while the fan moves. The script writes the whole
transformof a card; a hover transform of the page's own on the cards fights it.
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
Card fan is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called card fan hover effect, fan out cards on hover or card stack hover effect.
A deck of cards, each one a link: projects, products, articles, samples. At rest the cards sit in a tight pile, a few of them leaning out from under the top one. When the pointer reaches the pile, or the deck takes the focus, or a finger taps it, the cards spread into an arc like a hand of playing cards, each on its own delay and its own spring. The card under the pointer lifts, straightens, grows a little and comes to the front, its shadow lengthening as it rises, while its neighbours slide along the arc to make room. Leaving folds the fan back into the pile, in order. A click on a card follows its link. No library: a small spring per card, transforms only, and no frame asked for at rest. On a narrow screen the same hand is held tighter, so the fan always fits the deck's width. Under reduced motion there is no pile and no fan: the cards are laid in a plain row.
Updated
Questions
Put the custom attribute data-card-fan on the deck and data-card-fan-card on each card, a direct child of the deck. Settings are attributes on the deck, for instance data-card-fan-spread="60". On the Designer canvas the cards show in a row, because the Designer never runs the Embed: the pile and the fan show in preview and on the published site.
Neither. Each card runs on a small spring written in the engine, with transforms only and no library, so a card sent somewhere else in mid flight curves towards its new place instead of restarting. The loop stops as soon as every card is settled, open or closed.
Raise data-card-fan-spread, the angle in degrees between the first card and the last one of the open fan, 46 by default. Lower gives a flat row of barely leaning cards; past 90 the end cards lie on their side. data-card-fan-radius is the arc the cards sit on, and the script shortens it when the deck is too narrow for the fan.
Something around the deck clips it: overflow: hidden on the deck, on the section or on a parent cuts the lifted card and the two ends of the fan. The deck is only as tall as one card, and the open fan spills above it by lift and below it by the foot of the arc, so give the deck a padding. Give each card a width of its own too, since a card at width: 100% leaves the fan no room to open.
On touch each step takes a tap: one on the pile opens it, one on a card lifts it, one on the lifted card follows its link, and a tap anywhere else folds the fan. From the keyboard the deck is one Tab stop, the arrows move from card to card, Enter follows the card and Escape folds the fan. Under prefers-reduced-motion there is no pile and no fan, and the cards are laid in a plain row.