Panel accordion
A row of tall picture panels where the one under the pointer opens wide and the others fold to slim strips under a veil of the page colour.
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 row needs a height: the frames take what the words leave, and a row with no height shows no picture. The stylesheet gives one by default, which any class of the page replaces. The row is also a size container, so its width cannot come from its content. -
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-panel-accordion Marks the box that holds the panels, the row. It takes no value: leave the value field empty. That box is what lays the panels side by side, what the effect is mounted on and what carries the settings. Without it on any box of the page, the embed marks the parent of the first panel it finds next to itself, and says so in the console when there is none. data-panel-accordion-item Marks one panel, inside that box. It takes no value. A panel is what the pointer reaches, what a tap opens and what takes the focus, so in the preview it is the link itself. Without it on any panel, nothing is mounted and the console says so. [ Required ] data-panel-accordion-frame Marks the box that clips the picture of a panel, and that carries the veil. It takes no value. Without it the picture is clipped by the panel itself, over the words, and no veil shows. [ Optional ] data-panel-accordion-image Marks the picture of a panel, an Image inside the frame. It takes no value. It is held as wide as the open panel and slides inside the frame as the panel moves. Without it the image is laid out by the page alone and is cropped anew at every width. [ Optional ] data-panel-accordion-fold Marks the block of words only the open panel shows. It takes no value, and it holds one wrapper that holds the lines. It is laid out at the width of the open panel, and on a narrow screen it is what opens in height. Without it the words wrap again at every width of their panel. [ Optional ] data-panel-accordion-line Marks one line that rises out of a mask when its panel opens: the name, the text. It takes no value. Without it the words are simply there, hidden only by the edge of a folded panel. [ Optional ] data-panel-accordion-number Marks the number of a panel, which turns to the ink when the panel is open. It takes no value. Without it the number keeps its own colour. [ Optional ] data-panel-accordion-state Written by the engine: still or live on the row, open on the open panel, before and after on the folded ones, nothing when every panel is folded. Never written by hand; the stylesheet reads it for every size and every move. [ 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-panel-accordion-tint #f4ece0 Colour of the veil laid over the picture of every folded panel, the cream of the page in the preview. Options: any CSS colour; the colour of the page fades the folded panels into it, the ink #15100c puts them in the shade; taken out of this block, --panel-accordion-tint on the row takes over. data-panel-accordion-veil 0.5 Strength of that veil on a folded panel, 0 to 1; the open panel never carries it. data-panel-accordion-ratio 3.6 Size of the open panel against a folded one: its width in the row, the height of its picture in the column. data-panel-accordion-strip clamp(44px, 13vw, 64px) Height of the picture of a folded panel once the row is a column, any CSS unit; the open picture is strip times ratio. data-panel-accordion-gap clamp(4px, 0.5vw, 8px) Space between two panels, in the row and in the column, any CSS unit. data-panel-accordion-parallax clamp(14px, 2.2vw, 36px) How far a picture travels inside its frame while its panel opens or folds, any CSS unit; the picture is held that much wider than the open panel on each side. data-panel-accordion-duration 0.9 Time a panel takes to open, in seconds; the veil and the pictures move on the same clock. data-panel-accordion-ease cubic-bezier(0.5, 0, 0.1, 1) Pace of the opening, any CSS easing. data-panel-accordion-text-delay 0.28 Wait, in seconds, before the words of the open panel rise out of their mask; each line then follows the one before by 0.07 s. data-panel-accordion-rest 1 Number of the panel open when the page loads, counted from 1. Options: 1 to the number of panels; 0 leaves every panel folded, all the same width, until one is reached. data-panel-accordion-back 0 What the row does when the pointer or the focus leaves it. Options: 0 keeps the last panel open; 1 goes back to the rest panel, or folds every panel when rest is 0.
Two files and their pictures (7), 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
PanelAccordion.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-panel-accordion-tint #f4ece0 Colour of the veil laid over the picture of every folded panel, the cream of the page in the preview. Options: any CSS colour; the colour of the page fades the folded panels into it, the ink #15100c puts them in the shade; taken out of this block, --panel-accordion-tint on the row takes over. data-panel-accordion-veil 0.5 Strength of that veil on a folded panel, 0 to 1; the open panel never carries it. At 0 no veil, the folded pictures stay as bright as the open one Past 0.8 the folded panels are flat strips of the tint data-panel-accordion-ratio 3.6 Size of the open panel against a folded one: its width in the row, the height of its picture in the column. At 1 nothing opens; near 2 the open panel barely stands out Past 6 the folded panels are slivers and the open one takes the row data-panel-accordion-strip clamp(44px, 13vw, 64px) Height of the picture of a folded panel once the row is a column, any CSS unit; the open picture is strip times ratio. Thin bands of colour, a short column Folded panels that already show their picture, a long column data-panel-accordion-gap clamp(4px, 0.5vw, 8px) Space between two panels, in the row and in the column, any CSS unit. At 0 the pictures touch Wide gutters, and the open panel narrower by as much data-panel-accordion-parallax clamp(14px, 2.2vw, 36px) How far a picture travels inside its frame while its panel opens or folds, any CSS unit; the picture is held that much wider than the open panel on each side. At 0 the picture is pinned to the centre of its frame A picture that visibly drifts, and that is cropped by as much on each side data-panel-accordion-duration 0.9 Time a panel takes to open, in seconds; the veil and the pictures move on the same clock. A snap A slow, heavy opening that a quick pass of the pointer never finishes data-panel-accordion-ease cubic-bezier(0.5, 0, 0.1, 1) Pace of the opening, any CSS easing. linear keeps one even pace, mechanical A curve with a long tail, cubic-bezier(0.16, 1, 0.3, 1) for one, starts at once and settles slowly data-panel-accordion-text-delay 0.28 Wait, in seconds, before the words of the open panel rise out of their mask; each line then follows the one before by 0.07 s. At 0 the words rise with the panel, while it is still narrow Words that come once the panel has settled data-panel-accordion-rest 1 Number of the panel open when the page loads, counted from 1. Options: 1 to the number of panels; 0 leaves every panel folded, all the same width, until one is reached. data-panel-accordion-back 0 What the row does when the pointer or the focus leaves it. Options: 0 keeps the last panel open; 1 goes back to the rest panel, or folds every panel when rest is 0. -
Drive it from JavaScript
When something else than the pointer has to open a panel, when the panels change after load, or when the page reacts to the panel that is open.const element = document.querySelector('[data-panel-accordion]'); element.addEventListener('panelaccordion:open', (e) => { console.log(e.detail); }); const row = PanelAccordion.mount(element); // the row auto() mounted, or a new one row.open(2); // opens the third panel row.close(); // folds every panel element.addEventListener('panelaccordion:open', (e) => { console.log('panel', e.detail.from, 'to', e.detail.index); }); // When the row leaves the page, or before its panels are rendered again: PanelAccordion.destroy(element); // takes every state and attribute off
-
Fit it to your page
- Structure: a box marked
data-panel-accordion, the row, and inside it one panel per entry markeddata-panel-accordion-item. In a panel, the box that clips the picture is markeddata-panel-accordion-frame, the picturedata-panel-accordion-image, the block of words that only the open panel showsdata-panel-accordion-fold, and each line that risesdata-panel-accordion-line. In the reference markup a panel is a link, so the whole panel is one target and one stop for the keyboard. - The row needs a height. The frames take the height the row has left once the words are placed, so a row with no height shows no picture. The stylesheet gives one by default,
clamp(360px, 70vh, 820px), written in:where()so any class of the page replaces it. In the column the height is the panels' own. - One wrapper inside the fold.
data-panel-accordion-foldholds exactly one child, and that child holds the lines. In the column the fold is what opens and closes in height, and its child is what it clips. - One setting for every row of the page, without opening the file: declare
window.PanelAccordionSettings = { ratio: 5 }in a script placed beforepanel-accordion.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-panel-accordion-*attribute on the row itself (data-panel-accordion-ratio="5"), so one row can still differ from the page-wide setting. - The veil colour:
data-panel-accordion-tint="#15100c"on the row, ortintin the page-wide block. On a dark page, give it the colour of the page and the folded panels sink into it. - What the script writes.
data-panel-accordion-stateon the row,stillwhile nothing may move (the first frame, and reduced motion) andliveafter it; on each panel,openfor the open one,beforeandafterfor the folded ones on either side, and nothing when every panel is folded. Each panel also getsaria-expanded, atabindex(0 on the open panel, -1 on the others) and, when it is neither a link nor a button,role="button". The settings go on the row as--panel-accordion-*custom properties, and each line gets--panel-accordion-order, its rank in its panel. - **From JavaScript, beyond
mount,autoanddestroy**: the api carrieselement,settings,panels,open(index)(counted from 0),close()which folds every panel,index()which answers the open panel or -1, andrefresh()which reads the settings again. The row emitspanelaccordion:ready{ panels, index }once mounted,panelaccordion:open{ index, from, panel }each time another panel opens, andpanelaccordion:close{ index }when the last open panel folds. All bubble. - **
destroy()leaves the row as it was**: the listeners gone, the states, the custom properties and the ARIA attributes taken off or put back to what the markup said, every panel the same width again. - Keyboard: the row is one Tab stop, the open panel. The arrow keys move the focus to the next or the previous panel and wrap around, Home and End go to the first and the last, and the panel that has the focus opens. Enter follows the link of the panel, as on any link.
- Touch: a tap on a folded panel opens it and does not follow its link; a tap on the open panel does. Nothing follows the finger.
- Narrow screen: at 767px and under, the row is a column. A folded panel is its picture, one
striptall, over whatever stays outside the fold (the number, in the preview); the open one shows its pictureratiotimes taller and its words under it. The pictures then travel up and down. - **
prefers-reduced-motion**: the row staysstill. A panel still opens on hover, focus or tap, at once, with its words in place: nothing slides and nothing rises. - The words of the markup are the preview's, and they ship as they are. Put the user's own panels 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
- Without the stylesheet nothing opens. The script only writes states and custom properties; the widths, the mask and the pace are all rules of
panel-accordion.css. - The row is a size container (
container-type: inline-size), which is how a panel knows the width of the open one. Its own width therefore cannot come from its content: leave it a block, a flex or grid child that stretches, or give it a width. A row set towidth: max-contentor floated collapses to nothing. - The mechanics outweigh a single class. They are written under the row's attribute (
[data-panel-accordion] [data-panel-accordion-item]), so a class of the page cannot change the share of a panel, the place of a picture or the mask of a line by accident. The height of the row, and the top and the height of a picture, are the exceptions, in:where(): the preview's look uses the second to hold its pictures 160 % tall, set 27 % higher, and show the middle of each photograph. - **The picture is cropped like
object-fit: cover, at the size of the open panel plus the travel.** A folded panel shows the middle of it. A picture whose subject sits at one edge loses it in the strips; pick pictures that read from their centre. - A panel clips what it holds (
overflow: hidden), since the words are laid out at the width of the open panel and a folded one must hide them. A shadow or a badge meant to stick out of a panel is cut. - 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 row that already carries the effect returns the existing one. A page-wide block goes inwindow.PanelAccordionSettings, whichauto()reads itself; an attribute changed later is picked up byrefresh(). - Panels added after the mount are not part of the row. Call
PanelAccordion.destroy(row)thenPanelAccordion.mount(row). - The column switches at 767px of the window, not of the row. A row kept narrow on a wide screen, in a sidebar for one, stays a row.
- Old browsers without container query units (before Safari 16, Chrome 105, Firefox 110) size the pictures at their natural width instead; the panels still open.
- Without the stylesheet nothing opens. The script only writes states and custom properties; the widths, the mask and the pace are all rules of
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
Panel accordion is an animated navigation component for Webflow and vanilla JavaScript, with no library. Also called horizontal image accordion, image accordion on hover or expanding image panels.
A row of tall picture panels where the one under the pointer opens wide and the others fold to slim strips under a veil of the page colour. Each panel is one entry: a picture in a frame, and under it a number, a name and one line. The name and the line of the open panel rise out of a mask a little after the panel has started to move, one line after the other, and every picture slides a short way inside its frame, against the move of its panel, so the opening reads as heavy and smooth. Hover or focus opens a panel with a mouse, a tap opens it on a touch screen and a second tap follows its link, and the arrow keys walk the row, which is one Tab stop. On a narrow screen the row becomes a column and the panels open in height. No library: the sizes and the pace are CSS transitions on flex shares and transforms, and the script only writes the state, the settings and the keyboard. Under reduced motion a panel still opens, at once.
Updated
Questions
Mark the row data-panel-accordion and each panel data-panel-accordion-item; inside a panel, the box that clips the picture takes data-panel-accordion-frame and the picture data-panel-accordion-image. Settings are custom attributes on the row, for instance data-panel-accordion-rest="3" for the panel open when the page loads. The row needs a height, and the stylesheet gives it clamp(360px, 70vh, 820px) until a class of yours replaces it.
With CSS. Everything that moves is a transition on flex-grow and on transforms, with no library; the script never measures a box and never runs a frame loop, it only writes the state, the settings and the keyboard. Without panel-accordion.css nothing opens.
Set data-panel-accordion-ratio, the size of the open panel against a folded one, 3.6 by default. Near 2 the open panel barely stands out; past 6 the folded panels are slivers and the open one takes the row. data-panel-accordion-duration, 0.9 by default, is the time in seconds a panel takes to open.
At 767px of the window and under, the row becomes a column and the panels open in height. A tap on a folded panel opens it without following its link, and a tap on the open panel follows it. In the column a folded picture is one strip tall, clamp(44px, 13vw, 64px) by default, and the open one ratio times taller.
Yes. The row is one Tab stop: the arrow keys move the focus to the next or the previous panel and wrap around, Home and End go to the first and the last, and the panel that has the focus opens. Each panel gets aria-expanded, and under prefers-reduced-motion a panel still opens, at once, with its words in place.