Expanding Panel Row
A row of panels where the hovered one grows and its neighbours compress.
- Category
- Patterns
- Added
- 2026-09-20
- Deps
- none
- Updated
- 2026-09-20
Props
"use client";
import { useState } from "react";
type Panel = { id: string; label: string; image: string };
export function HoverExpandPanels({
panels,
expandRatio = 4,
showLabels = true,
vertical = false,
}: {
panels: Panel[];
expandRatio?: number;
/** The label that resolves once a panel has room for it. */
showLabels?: boolean;
/** Stack the panels instead of laying them out in a row. */
vertical?: boolean;
}) {
const [active, setActive] = useState<number | null>(null);
return (
<div
onMouseLeave={() => setActive(null)}
className={
"flex h-64 w-full gap-1.5 overflow-hidden rounded-lg " +
(vertical ? "flex-col" : "flex-row")
}
>
{panels.map((panel, i) => {
const isActive = active === i;
return (
<button
key={panel.id}
onMouseEnter={() => setActive(i)}
onFocus={() => setActive(i)}
onBlur={() => setActive(null)}
aria-label={panel.label}
className="group relative min-w-0 overflow-hidden rounded-md transition-[flex-grow] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]"
style={{
/*
* flex-grow, not measured pixel widths. The row always sums to
* the container no matter how many panels there are or how wide
* it is, so nothing needs recomputing on resize and panels can
* never drift outside the box. min-w-0 lets them compress past
* their content's intrinsic width.
*/
flexGrow: isActive ? expandRatio : 1,
flexBasis: 0,
}}
>
<img
src={panel.image}
alt=""
className="absolute inset-0 h-full w-full object-cover"
/>
<span className="absolute left-2 top-2 font-mono text-[10px] tracking-[0.14em] text-white/70">
{panel.id}
</span>
{showLabels && (
<span
className={
"absolute bottom-2 left-2 whitespace-nowrap text-sm font-bold text-white transition-all duration-300 " +
(isActive ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0")
}
style={{ transitionDelay: isActive ? "140ms" : "0ms" }}
>
{panel.label}
</span>
)}
</button>
);
})}
</div>
);
}
About this pattern
The horizontal accordion is a familiar gallery shape, and the usual implementation measures the container, divides it up and animates pixel or percentage widths — which means a resize observer, arithmetic that has to be redone whenever the panel count changes, and rounding errors that let the row overflow by a pixel or two. Driving it with flex-grow removes all of that: the active panel grows to a ratio, the rest stay at one, and the browser's own layout algorithm guarantees the row sums to the container at every intermediate frame. Adding min-w-0 is what lets a panel compress below the intrinsic width of its content, which is the usual reason a naive version refuses to shrink past a certain point. The timing detail that makes it feel finished is the label delay: titles fade in about 140ms after the width starts moving, so they arrive into space that already exists rather than reflowing during the transition. Because collapsed panels hide their titles, each panel needs its own accessible label, and focus must be wired alongside hover or the row is unusable from the keyboard.
Curator’s note
Built for viberdy 2.0. Driving it with flex-grow instead of measured widths is what makes it survive resize and an arbitrary panel count.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Hover Reveal Text Gallery
patternsA list of text rows, each hiding a photograph that wipes open under the cursor and follows it.
Masonry Image Hover Grid
assetsA CSS-columns masonry gallery with a scale + caption reveal on hover.
Link Peek Preview
patternsAn index list that floats a velocity-tilted preview card beside the pointer.