More Layouts Snippets
Image Accordion — Expanding Panels HTML CSS JS Snippet
Image Accordion · Layouts · Plain HTML, CSS & JS · Live preview
What's included
Features
flex-grow: 6 vs 1; a single transition: flex-grow animates the expand/collapse with no width calculations.writing-mode: vertical-rl keeps labels readable in narrow slivers and prevents text overflow when collapsed.setActive clears .active everywhere and sets it on one panel — both click and hover route through it, so states never conflict.matchMedia('(hover: hover)') adds mouseenter expansion only on pointer devices, leaving phones with reliable tap-to-expand.--g variable, so swapping gradients for real url() images is a one-line change per panel.::after keeps white titles and descriptions readable over any image or colour.About this UI Snippet
Image Accordion — Flex-Grow Expansion, Vertical Labels & Reveal-on-Expand Captions

The horizontal image accordion is a striking way to present a small set of categories or destinations in a compact, interactive strip: a row of panels where hovering or clicking one makes it expand to fill most of the width while the others shrink to slivers, revealing a caption on the active panel. It is a staple of travel sites, portfolios, and category landing pages. This snippet implements it in plain HTML, CSS, and vanilla JavaScript using nothing but flexbox and transitions — no fixed widths, no JavaScript animation loop.
Flex-grow is the whole trick
Each panel is a flex child with flex-grow: 1, so by default they share the row equally. The active panel gets .active which sets flex-grow: 6, claiming six times the space of its siblings. Because flex-grow is animatable, a single transition: flex-grow .5s produces the smooth expand-and-collapse — the browser distributes the remaining space and every panel eases to its new size simultaneously. Using flex-grow rather than width means the layout stays fluid: it adapts to any container width and any number of panels without recalculating pixels.
Collapsed vertical label, expanded horizontal caption
A collapsed panel is narrow, so its label is rendered vertically with writing-mode: vertical-rl — readable in the sliver without overflowing. When a panel becomes active, that vertical label fades out and a separate .ia-info block (a large title plus a one-line description) fades and slides up from the bottom-left with a slight delay, so the caption appears after the panel has begun widening. The emoji marker also slides from centred to left-aligned as the panel expands. These small staggered transitions are what make the interaction feel designed rather than abrupt.
Click and hover, without conflict
setActive is the single source of truth: it clears .active from every panel and sets it on the chosen one. The inline onclick makes it work on touch devices and as a stable demo. To add the classic hover-to-expand behaviour without breaking touch, the JS checks window.matchMedia('(hover: hover)') and only attaches mouseenter handlers on devices that actually have a hovering pointer — so phones get tap-to-expand and desktops get hover-to-expand, each using the same setActive function.
Image-ready
The panels use gradient backgrounds with emoji placeholders so the snippet is self-contained, but each panel's background is a single --g custom property — swap it for background-image: url(...) (or set --g to an image) to drop in real photos, and a built-in bottom gradient overlay keeps the white caption legible over any image.
Pair this accordion with a scroll-snap gallery for swipeable galleries, a photo gallery grid, or a bento grid layout.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA row of five gradient panels (Mountains, Ocean, Forest, Desert, City) appears with the first one expanded and captioned.
- 2Click a panelClick Ocean — it expands to fill most of the row, the others collapse to slivers, and its caption fades up while its vertical label disappears.
- 3Hover on desktopOn a device with a mouse, simply move across the panels — each expands on hover via the same
setActivelogic. - 4Read the captionsThe expanded panel shows a large title and a one-line description; collapsed panels show only a compact vertical label.
- 5Swap in real photosSet each panel's
--gcustom property to aurl(...)image; the built-in dark gradient overlay keeps the caption readable. - 6Add or remove panelsCopy or delete a
.ia-panel— flex-grow redistributes the space automatically, no width math required.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each panel's background comes from its --g custom property. Set it to an image, e.g. style="--g:url('/img/ocean.jpg') center/cover", or change the .ia-panel rule to use background-image. The built-in ::after gradient overlay already darkens the bottom so the white caption stays readable over any photo.
Width animations require knowing or computing exact pixel values and break when the container resizes or the panel count changes. flex-grow is share-based: the active panel claims a larger share and the browser redistributes the rest, so the accordion is fully fluid and responsive with one transition and zero layout math.
Remove the matchMedia block at the bottom of the JS — that is the only thing adding hover behaviour. The inline onclick="setActive(this)" remains, giving you pure click-to-expand on every device. Conversely, to make it hover-only on desktop, keep the block and remove the inline onclick.
Add tabindex="0" and role="button" to each panel and a keydown handler that calls setActive on Enter/Space, so keyboard users can expand panels. Give each panel an aria-label with its title, and if the panels link somewhere, use real <a> elements as the panel so they are focusable and announced natively.
In React, store the active index in useState and set each panel's class from index === active; handle click (and optional hover) to update it. In Vue, use a ref for the active index with :class bindings in a v-for. In Angular, track the active index and bind [class.active]. The flex-grow transition and caption CSS port unchanged.