Hover Expand Gallery — Free HTML CSS JS Accordion Gallery
Hover Expand Gallery · Media · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="he-gallery" id="heGallery">
<div class="he-panel is-open" style="--g:linear-gradient(160deg,#f97316,#db2777)" data-title="Kyoto">
<span class="he-cap">Kyoto</span>
</div>
<div class="he-panel" style="--g:linear-gradient(160deg,#6366f1,#06b6d4)" data-title="Reykjavik">
<span class="he-cap">Reykjavik</span>
</div>
<div class="he-panel" style="--g:linear-gradient(160deg,#10b981,#84cc16)" data-title="Patagonia">
<span class="he-cap">Patagonia</span>
</div>
<div class="he-panel" style="--g:linear-gradient(160deg,#8b5cf6,#ec4899)" data-title="Marrakesh">
<span class="he-cap">Marrakesh</span>
</div>
<div class="he-panel" style="--g:linear-gradient(160deg,#0ea5e9,#6366f1)" data-title="Lofoten">
<span class="he-cap">Lofoten</span>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0b12;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.he-gallery{display:flex;gap:10px;width:min(720px,94vw);height:300px}
.he-panel{position:relative;flex:1;min-width:46px;border-radius:16px;background:var(--g);cursor:pointer;overflow:hidden;flex-grow:1;transition:flex-grow .5s cubic-bezier(.22,1,.36,1)}
.he-panel.is-open{flex-grow:6}
.he-panel::after{content:'';position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.55),transparent 55%)}
.he-cap{position:absolute;left:14px;bottom:14px;z-index:1;color:#fff;font-size:18px;font-weight:700;letter-spacing:.01em;white-space:nowrap;opacity:0;transform:translateY(8px);transition:opacity .3s .12s,transform .3s .12s}
.he-panel.is-open .he-cap{opacity:1;transform:none}var gallery = document.getElementById('heGallery');
var panels = Array.prototype.slice.call(gallery.querySelectorAll('.he-panel'));
function open(panel) {
panels.forEach(function (p) { p.classList.toggle('is-open', p === panel); });
}
panels.forEach(function (panel) {
// Expand on hover for pointers, and on click/tap for touch devices.
panel.addEventListener('mouseenter', function () { open(panel); });
panel.addEventListener('click', function () { open(panel); });
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Hover Expand Gallery — A Horizontal Flex Accordion

The hover expand gallery is the horizontal image accordion where hovering a slim panel makes it grow to fill most of the row while its neighbours shrink, revealing a caption as it opens. This snippet builds it with plain HTML, CSS flexbox transitions, and a tiny vanilla JavaScript class toggle — no widths to calculate and no library.
Flex-grow does the layout
All panels live in a flex row and share the available space through flex-grow. At rest every panel has the same grow value; the open panel is given a much larger one (flex-grow: 6 versus 1). Because the panels divide the row proportionally, raising one panel's grow automatically squeezes the others — you never set explicit pixel widths, so the gallery is fully responsive and adapts to any container size. A min-width keeps the collapsed panels wide enough to remain clickable.
Animating an intrinsic property
The expansion is animated by putting a transition on flex-grow itself with an ease-out cubic-bezier, so the panels glide between their collapsed and expanded sizes. Animating flex-grow (rather than width) is what lets the whole row rebalance smoothly in one motion: as the active panel grows, the rest contract in the same frame because they're all sharing the same flexible space.
Revealing the caption
Each panel's caption starts hidden — opacity: 0 and nudged down with translateY — and only animates in when the panel gains the is-open class, with a small delay so the text appears after the panel has begun widening. This staggering makes the label feel like it belongs to the expansion rather than popping in abruptly. A bottom gradient overlay (::after) keeps the white caption readable over any panel colour.
One class, mutually exclusive
The JavaScript is deliberately minimal: an open() function toggles a single is-open class so exactly one panel is expanded at a time, and everything else — sizing, caption reveal, overlay — is driven by CSS from that one class. This keeps the logic trivial and the animation entirely declarative.
Hover and touch
Panels expand on mouseenter for pointer devices and also on click, so the gallery works on touchscreens where there is no hover — a tap opens a panel just as a hover does on desktop. This dual binding is the simple, reliable way to make a hover effect usable everywhere.
Customizing it
Swap the gradient backgrounds for real images via background-image, change the open grow ratio for a more or less dramatic expansion, adjust the timing, or add more panels — the flex math handles any count. Pair it with an image accordion, a photo gallery, or a scroll snap gallery.
Step by step
How to Use
- 1Paste HTML, CSS, and JSFive colored panels render with one open.
- 2Hover a panelIt expands and the others shrink smoothly.
- 3See the captionThe label fades in as the panel opens.
- 4Tap on mobileA tap opens a panel where hover is absent.
- 5Use real imagesSet background-image on each panel.
- 6Tune the spreadChange the open panel's flex-grow ratio.
Real-world uses
Common Use Cases
flex-grow transitions so the row always fills the available width.Got questions?
Frequently Asked Questions
All panels share the row through flex-grow, so raising one panel's grow value automatically squeezes the others within the same flexible space. Transitioning flex-grow lets the whole row rebalance in one smooth motion, and because no pixel widths are set, the gallery stays fully responsive to its container.
Each caption starts at opacity 0 and nudged down with translateY. When the panel gains the is-open class, the caption transitions in with a short delay, so it appears just after the panel begins widening. That stagger makes the label feel part of the expansion rather than popping in abruptly.
Yes. Panels expand on mouseenter for pointers and also on click, so a tap opens a panel on touchscreens where hover does not exist. Binding both events is the simplest reliable way to make the hover behaviour usable on mobile and desktop alike.
The open function toggles a single is-open class so exactly one panel carries it. All sizing, the caption reveal, and the overlay are driven by CSS from that one class, which keeps the JavaScript trivial and the animation entirely declarative.
Keep the open index in state and render panels from an array, applying the is-open class where the index matches. Bind onMouseEnter and onClick to set the index. All the animation is CSS, so nothing else changes; in Tailwind use grow utilities with arbitrary flex-grow values and a transition on flex-grow.