Image Accordion — Expanding Panels HTML CSS JS Snippet

Image Accordion · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Pure flex-grow expansion
Active panel sets flex-grow: 6 vs 1; a single transition: flex-grow animates the expand/collapse with no width calculations.
Fluid, panel-count agnostic
Because layout is share-based, it adapts to any container width and any number of panels without recalculating pixels.
Vertical collapsed labels
writing-mode: vertical-rl keeps labels readable in narrow slivers and prevents text overflow when collapsed.
Staggered caption reveal
The active caption fades and slides up with a slight delay after the panel widens, so content appears in sequence, not all at once.
Single source of truth
setActive clears .active everywhere and sets it on one panel — both click and hover route through it, so states never conflict.
Hover-capable detection
matchMedia('(hover: hover)') adds mouseenter expansion only on pointer devices, leaving phones with reliable tap-to-expand.
Custom-property backgrounds
Each panel reads its fill from a --g variable, so swapping gradients for real url() images is a one-line change per panel.
Legibility overlay
A bottom-to-top dark gradient ::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

Screenshot of the Image Accordion snippet rendered live

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

  1. 1
    Paste HTML, CSS, and JSA row of five gradient panels (Mountains, Ocean, Forest, Desert, City) appears with the first one expanded and captioned.
  2. 2
    Click 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.
  3. 3
    Hover on desktopOn a device with a mouse, simply move across the panels — each expands on hover via the same setActive logic.
  4. 4
    Read the captionsThe expanded panel shows a large title and a one-line description; collapsed panels show only a compact vertical label.
  5. 5
    Swap in real photosSet each panel's --g custom property to a url(...) image; the built-in dark gradient overlay keeps the caption readable.
  6. 6
    Add or remove panelsCopy or delete a .ia-panel — flex-grow redistributes the space automatically, no width math required.

Real-world uses

Common Use Cases

Travel and destination showcases
Present destinations or categories as expanding photo panels. Combine with a photo gallery for the full set.
Portfolio and project highlights
Show a handful of featured projects in a compact strip that expands on interaction, linking to case studies.
Category landing strips
An e-commerce homepage row of shoppable categories; pair with a faceted filter sidebar on the listing page.
Service or feature overviews
Each panel describes a service; expanding reveals a short pitch — a more visual alternative to a feature cards grid.
Team or talent spotlights
Panels of people that expand to show a name and role, an interactive twist on a team card section.
Event or program lineups
Showcase festival stages, tracks, or programs as expandable image panels with a caption per item.

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.