Circle Thumbs Gallery
Original · freeA row of circular thumbnails that scale and show a caption on hover.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/gallery-circle-thumbs.json"use client";
import { useRef, useState } from "react";
import { motion, useInView } from "motion/react";
type Thumb = {
src: string;
title: string;
category: string;
caption: string;
alt: string;
};
const THUMBS: Thumb[] = [
{
src: "/img/gallery/g03.webp",
title: "Coastal Dawn",
category: "Landscape",
caption: "First light spilling over the headland cliffs at Praia da Marinha.",
alt: "Golden sunrise breaking over rugged coastal cliffs above a calm sea",
},
{
src: "/img/gallery/g08.webp",
title: "Studio Portrait",
category: "People",
caption: "Soft window light, 85mm, shot for the autumn lookbook.",
alt: "Portrait of a person lit softly by natural window light in a studio",
},
{
src: "/img/gallery/g14.webp",
title: "Market Colours",
category: "Street",
caption: "Spice stalls at Marrakech's Jemaa el-Fnaa, mid-morning rush.",
alt: "Vibrant spice and produce stall at a busy outdoor market",
},
{
src: "/img/gallery/g21.webp",
title: "Alpine Silence",
category: "Nature",
caption: "Fresh powder above the tree line, Dolomites in January.",
alt: "Snow-covered alpine peaks under a clear winter sky",
},
{
src: "/img/gallery/g05.webp",
title: "Neon After Rain",
category: "Urban",
caption: "Reflections on wet asphalt, Shinjuku after the evening storm.",
alt: "City street at night with neon signs reflected on wet pavement",
},
{
src: "/img/gallery/g27.webp",
title: "Ceramic Hands",
category: "Craft",
caption: "A potter centres clay on the wheel in a Lisbon workshop.",
alt: "Close-up of hands shaping wet clay on a pottery wheel",
},
{
src: "/img/gallery/g11.webp",
title: "Desert Lines",
category: "Landscape",
caption: "Wind-carved dunes at golden hour, Erg Chebbi.",
alt: "Sweeping sand dunes with sharp ridge lines at sunset",
},
{
src: "/img/gallery/g33.webp",
title: "Harbour Blue",
category: "Travel",
caption: "Fishing boats resting in the still morning harbour of Chefchaouen.",
alt: "Small wooden fishing boats moored in a calm blue harbour",
},
{
src: "/img/gallery/g18.webp",
title: "Forest Canopy",
category: "Nature",
caption: "Looking straight up through old-growth redwoods in fog.",
alt: "Tall forest trees seen from below with fog in the canopy",
},
{
src: "/img/gallery/g30.webp",
title: "Table Setting",
category: "Food",
caption: "Seasonal plating for the winter tasting menu.",
alt: "Elegantly plated dish on a rustic table with soft light",
},
];
export default function GalleryCircleThumbs() {
const sectionRef = useRef<HTMLElement>(null);
const inView = useInView(sectionRef, { once: true, margin: "-80px" });
const [active, setActive] = useState<number | null>(null);
return (
<section
ref={sectionRef}
className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 px-5 py-20 sm:px-8 sm:py-28 dark:from-zinc-950 dark:via-zinc-950 dark:to-black"
>
<style>{`
@keyframes gct-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes gct-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.gct-ring-spin { animation: gct-ring 14s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.gct-ring-spin { animation: none; }
.gct-thumb { transition: none !important; }
}
`}</style>
{/* ambient glow */}
<div
aria-hidden
className="pointer-events-none absolute left-1/2 top-0 h-[420px] w-[820px] -translate-x-1/2 rounded-full bg-gradient-to-tr from-indigo-300/30 via-violet-200/20 to-transparent blur-3xl dark:from-indigo-500/15 dark:via-violet-500/10"
/>
<div className="relative mx-auto max-w-6xl">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-widest text-indigo-600 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-indigo-300">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Field Portfolio
</span>
<h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Ten frames from the road
</h2>
<p className="mx-auto mt-3 max-w-md text-pretty text-sm leading-relaxed text-slate-600 sm:text-base dark:text-zinc-400">
Hover, tap or focus a portrait to bring it forward and read the story
behind the shot.
</p>
</div>
<ul
className="mt-14 flex flex-wrap items-start justify-center gap-x-6 gap-y-12 sm:gap-x-9 sm:gap-y-14"
onMouseLeave={() => setActive(null)}
>
{THUMBS.map((thumb, i) => {
const isActive = active === i;
const dimmed = active !== null && !isActive;
return (
<motion.li
key={thumb.src}
initial={{ opacity: 0, y: 24, scale: 0.9 }}
animate={
inView
? { opacity: 1, y: 0, scale: 1 }
: { opacity: 0, y: 24, scale: 0.9 }
}
transition={{
duration: 0.55,
delay: i * 0.06,
ease: [0.22, 1, 0.36, 1],
}}
className="relative flex w-28 flex-col items-center sm:w-36"
>
<button
type="button"
aria-label={`${thumb.title} — ${thumb.category}. ${thumb.caption}`}
aria-pressed={isActive}
onMouseEnter={() => setActive(i)}
onFocus={() => setActive(i)}
onBlur={() => setActive(null)}
onClick={() => setActive(isActive ? null : i)}
className="group relative outline-none"
>
{/* spinning gradient ring on active/hover */}
<span
aria-hidden
className={`gct-ring-spin absolute -inset-[6px] rounded-full bg-[conic-gradient(from_0deg,theme(colors.indigo.400),theme(colors.violet.400),theme(colors.emerald.400),theme(colors.indigo.400))] opacity-0 transition-opacity duration-500 group-hover:opacity-100 group-focus-visible:opacity-100 ${
isActive ? "opacity-100" : ""
}`}
/>
<span
className={`gct-thumb relative block aspect-square w-28 overflow-hidden rounded-full border-2 border-white shadow-lg shadow-slate-900/10 ring-1 ring-slate-900/5 transition-all duration-500 ease-out sm:w-36 dark:border-zinc-800 dark:shadow-black/40 dark:ring-white/10 ${
isActive
? "-translate-y-2 scale-110"
: "group-hover:-translate-y-1 group-hover:scale-105"
} ${dimmed ? "scale-95 opacity-55 saturate-50" : ""}`}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={thumb.src}
alt={thumb.alt}
loading="lazy"
draggable={false}
className="h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-110"
/>
<span
aria-hidden
className="pointer-events-none absolute inset-0 rounded-full bg-gradient-to-t from-slate-900/45 via-transparent to-white/10 opacity-60 transition-opacity duration-500 group-hover:opacity-30"
/>
</span>
{/* category chip */}
<span
className={`absolute -bottom-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full border border-slate-200 bg-white px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-slate-700 shadow-sm transition-all duration-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 ${
isActive
? "translate-y-1 opacity-100"
: "opacity-0 group-hover:translate-y-1 group-hover:opacity-100"
}`}
>
{thumb.category}
</span>
</button>
{/* caption reveal */}
<div
className={`mt-4 overflow-hidden text-center transition-all duration-500 ease-out ${
isActive
? "max-h-32 opacity-100"
: "max-h-0 opacity-0"
}`}
>
<p className="text-sm font-semibold text-slate-900 dark:text-white">
{thumb.title}
</p>
<p className="mx-auto mt-1 max-w-[15rem] text-pretty text-xs leading-relaxed text-slate-500 dark:text-zinc-400">
{thumb.caption}
</p>
</div>
</motion.li>
);
})}
</ul>
<p className="mt-16 text-center text-xs text-slate-400 dark:text-zinc-600">
{THUMBS.length} selects · shot 2024–2026 · available as archival prints
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Hover Caption Gallery
OriginalA responsive image grid where each tile subtly zooms and reveals a caption bar on hover.

Masonry Gallery
OriginalA Pinterest-style masonry gallery using CSS columns with varied-height images; tiles lift and brighten on hover.

Filterable Gallery
OriginalA filterable gallery with category tabs; clicking a tab animates the grid to show only that category.

Tilt Cards Gallery
OriginalA grid of image cards that tilt in 3D toward the cursor on hover, with a soft glare.

Lightbox Gallery
OriginalA thumbnail grid where clicking a thumb opens a full-screen lightbox with prev/next arrows and keyboard support.

Expanding Strip Gallery
OriginalA horizontal strip of image panels; hovering a panel expands it and shrinks the others to reveal its caption.

Marquee Gallery
OriginalTwo rows of images auto-scrolling in opposite directions, pausing on hover, with edge fade masks.

Hover Zoom Overlay Gallery
OriginalA grid where each image scales up under a dark gradient overlay revealing a title and a view button on hover.

Polaroid Stack Gallery
OriginalA scattered stack of slightly-rotated polaroid photos that straighten and lift on hover.
Featured Thumbs Gallery
OriginalA large featured image with a thumbnail row; selecting a thumbnail swaps the featured image with a crossfade.

Carousel Gallery
OriginalAn image carousel with arrows and dots, smooth slide transitions and autoplay that pauses on hover.

Duotone Hover Gallery
OriginalA grid where images sit under a coloured duotone wash that clears to full colour on hover.

