Expanding Image Panels
Tall image panels that widen on hover to reveal their text, turning into a swipeable row on phones. React and Tailwind CSS, no JavaScript.
ExpandingImagePanels.tsx
// Tall image panels that widen on hover (desktop) and reveal their text;
// a sideways-scrolling row of cards on phones and tablets.
// Pure Tailwind, no dependencies. Works with Tailwind v3 and v4.
type Panel = { name: string; region?: string; text?: string; image: string; href?: string };
type Props = {
heading?: string[];
intro?: string;
panels?: Panel[];
};
const img = (id: string) => `https://images.unsplash.com/photo-${id}?w=1400&q=80&auto=format&fit=crop`;
const DEFAULT_PANELS: Panel[] = [
{ name: "Amalfi Coast", region: "Italy · Mediterranean", text: "Lemon groves, cliffside villages and long lunches at anchor off Positano.", image: img("1612698093158-e07ac200d44e") },
{ name: "French Riviera", region: "France & Monaco", text: "From Saint-Tropez to Monaco: the original stage for life on the water.", image: img("1595138320174-a64d168e9970") },
{ name: "Dalmatian Coast", region: "Croatia · Adriatic", text: "A thousand islands, Venetian towns and the clearest water in Europe.", image: img("1565784623727-8cebf4712b2d") },
{ name: "The Grenadines", region: "Caribbean", text: "Turquoise reefs, private islands and the trade winds of winter.", image: img("1484821582734-6c6c9f99a672") },
{ name: "The Maldives", region: "Indian Ocean", text: "Twenty-six atolls of sandbanks, manta rays and absolute stillness.", image: img("1514282401047-d79a71a590e8") },
];
export default function ExpandingImagePanels({
heading = ["Five seas,", "one standard"],
intro = "From the Riviera in June to the Grenadines at Christmas, our fleet follows the sun.",
panels = DEFAULT_PANELS,
}: Props) {
return (
<section className="bg-white py-28 font-['Helvetica_Neue',Helvetica,Arial,sans-serif] text-[#0d1114] antialiased md:py-40">
<div className="mx-auto grid max-w-[100rem] gap-8 px-4 md:gap-12 md:px-8 lg:grid-cols-12 lg:items-end lg:px-12">
<h2 className="text-[clamp(2.25rem,4.5vw,4.75rem)] font-normal leading-none tracking-[-0.035em] lg:col-span-7">
{heading.map((line, i) => (
<span key={i} className="block">
{line}
</span>
))}
</h2>
{intro && <p className="text-base leading-relaxed text-[#0d1114]/60 md:text-lg lg:col-span-4 lg:col-start-9 lg:pb-3">{intro}</p>}
</div>
<div className="mt-14 flex snap-x snap-mandatory gap-3 overflow-x-auto px-4 [scrollbar-width:none] md:mt-20 md:px-8 lg:h-[78vh] lg:min-h-[36rem] lg:gap-2 lg:overflow-visible lg:px-12 [&::-webkit-scrollbar]:hidden">
{panels.map((p, i) => (
<a
key={p.name}
href={p.href ?? "#"}
className="group relative isolate block aspect-[3/4] w-[78vw] shrink-0 snap-start overflow-hidden bg-[#111c25] text-white transition-[flex-grow] duration-[1200ms] ease-[cubic-bezier(0.22,1,0.36,1)] sm:w-[46vw] lg:aspect-auto lg:w-auto lg:flex-1 lg:hover:flex-[2.6]"
>
<img
src={p.image}
alt={p.name}
loading="lazy"
className="absolute inset-0 -z-10 h-full w-full object-cover transition-transform duration-[1600ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-105"
/>
<div className="absolute inset-0 -z-10 bg-gradient-to-t from-[#060b10]/80 via-[#060b10]/10 to-[#060b10]/20" />
<span className="absolute left-6 top-6 text-xs tabular-nums text-white/70">{String(i + 1).padStart(2, "0")}</span>
<span className="absolute right-5 top-5 grid h-11 w-11 place-items-center rounded-full border border-white/40 opacity-0 transition-opacity duration-700 group-hover:opacity-100">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.1} aria-hidden className="h-4 w-4">
<path d="M7 17 17 7M8 7h9v9" />
</svg>
</span>
<div className="absolute inset-x-0 bottom-0 p-6 lg:p-8">
{p.region && (
<p className="mb-3 text-xs font-medium uppercase tracking-[0.08em] text-white/70 lg:opacity-0 lg:transition-opacity lg:duration-700 lg:group-hover:opacity-100">
{p.region}
</p>
)}
<h3 className="text-4xl font-normal leading-[0.95] lg:text-[clamp(2rem,2.6vw,3rem)]">{p.name}</h3>
{p.text && (
<p className="mt-4 max-w-sm text-sm leading-relaxed text-white/75 lg:max-h-0 lg:overflow-hidden lg:opacity-0 lg:transition-all lg:duration-700 lg:group-hover:max-h-32 lg:group-hover:opacity-100">
{p.text}
</p>
)}
</div>
</a>
))}
</div>
</section>
);
}
How to use it
- Make sure your Next.js or React project has Tailwind CSS set up.
- Create
components/ExpandingImagePanels.tsxand paste the code. - Import it into a page and replace the placeholder text and images with your own.
More free components
All gallery →
Ready for a faster site that's easier to edit?
Book a free 30-minute call. We'll review your current site and send a fixed-price plan. No pressure, no pitch.
Book Free Call No commitment required

