Circular Gallery
Twenty photo thumbnails arranged in a circle with CSS offset-path, hover-spread interactions, and anchor-positioned full-size preview.
Circular Gallery
Twenty photo thumbnails are positioned along a circle using CSS Motion Path (offset-path: circle()). Hovering a card triggers a ripple effect — up to four neighboring cards spread apart using --d offset variables set by :has() + adjacent-sibling combinators, with no JavaScript. Clicking a card targets it via its #item-N hash anchor, revealing a full-size circular preview at the center. The preview and tooltip use CSS Anchor Positioning with position: fixed — which works self-contained because the wrapper’s transform: rotate() makes it a containing block for fixed descendants.
Requires Chrome 116+ for CSS Anchor Positioning. An @property declaration makes the rotation angle animatable.
Basic Usage
Circular Gallery
<section class="art-circular-gallery">
<div id="cg-item-1" data-title="Yoni Kaplan-Nadel" style="--i:1; --bg-img:url(https://picsum.photos/id/27/1200/1200)">
<a href="#cg-item-1"><img src="https://picsum.photos/id/27/1200/1200" alt="Yoni Kaplan-Nadel"></a>
</div>
<div id="cg-item-2" data-title="Alejandro Escamilla" style="--i:2; --bg-img:url(https://picsum.photos/id/25/1200/1200)">
<a href="#cg-item-2"><img src="https://picsum.photos/id/25/1200/1200" alt="Alejandro Escamilla"></a>
</div>
<div id="cg-item-3" data-title="Gabriel Santiago" style="--i:3; --bg-img:url(https://picsum.photos/id/372/1200/1200)">
<a href="#cg-item-3"><img src="https://picsum.photos/id/372/1200/1200" alt="Gabriel Santiago"></a>
</div>
<div id="cg-item-4" data-title="Michael Quinn" style="--i:4; --bg-img:url(https://picsum.photos/id/380/1200/1200)">
<a href="#cg-item-4"><img src="https://picsum.photos/id/380/1200/1200" alt="Michael Quinn"></a>
</div>
<div id="cg-item-5" data-title="Chris Brignola" style="--i:5; --bg-img:url(https://picsum.photos/id/392/1200/1200)">
<a href="#cg-item-5"><img src="https://picsum.photos/id/392/1200/1200" alt="Chris Brignola"></a>
</div>
<div id="cg-item-6" data-title="Matteo Minelli" style="--i:6; --bg-img:url(https://picsum.photos/id/456/1200/1200)">
<a href="#cg-item-6"><img src="https://picsum.photos/id/456/1200/1200" alt="Matteo Minelli"></a>
</div>
<div id="cg-item-7" data-title="Matthew Clark" style="--i:7; --bg-img:url(https://picsum.photos/id/469/1200/1200)">
<a href="#cg-item-7"><img src="https://picsum.photos/id/469/1200/1200" alt="Matthew Clark"></a>
</div>
<div id="cg-item-8" data-title="Volkan Olmez" style="--i:8; --bg-img:url(https://picsum.photos/id/497/1200/1200)">
<a href="#cg-item-8"><img src="https://picsum.photos/id/497/1200/1200" alt="Volkan Olmez"></a>
</div>
<div id="cg-item-9" data-title="Jeff Sheldon" style="--i:9; --bg-img:url(https://picsum.photos/id/515/1200/1200)">
<a href="#cg-item-9"><img src="https://picsum.photos/id/515/1200/1200" alt="Jeff Sheldon"></a>
</div>
<div id="cg-item-10" data-title="Christian Holzinger" style="--i:10; --bg-img:url(https://picsum.photos/id/521/1200/1200)">
<a href="#cg-item-10"><img src="https://picsum.photos/id/521/1200/1200" alt="Christian Holzinger"></a>
</div>
<div id="cg-item-11" data-title="Artur Pokusin" style="--i:11; --bg-img:url(https://picsum.photos/id/549/1200/1200)">
<a href="#cg-item-11"><img src="https://picsum.photos/id/549/1200/1200" alt="Artur Pokusin"></a>
</div>
<div id="cg-item-12" data-title="Sam Wheeler" style="--i:12; --bg-img:url(https://picsum.photos/id/569/1200/1200)">
<a href="#cg-item-12"><img src="https://picsum.photos/id/569/1200/1200" alt="Sam Wheeler"></a>
</div>
<div id="cg-item-13" data-title="Griffin Keller" style="--i:13; --bg-img:url(https://picsum.photos/id/637/1200/1200)">
<a href="#cg-item-13"><img src="https://picsum.photos/id/637/1200/1200" alt="Griffin Keller"></a>
</div>
<div id="cg-item-14" data-title="Fré Sonneveld" style="--i:14; --bg-img:url(https://picsum.photos/id/641/1200/1200)">
<a href="#cg-item-14"><img src="https://picsum.photos/id/641/1200/1200" alt="Fré Sonneveld"></a>
</div>
<div id="cg-item-15" data-title="Luke Pamer" style="--i:15; --bg-img:url(https://picsum.photos/id/669/1200/1200)">
<a href="#cg-item-15"><img src="https://picsum.photos/id/669/1200/1200" alt="Luke Pamer"></a>
</div>
<div id="cg-item-16" data-title="Joshua Earle" style="--i:16; --bg-img:url(https://picsum.photos/id/685/1200/1200)">
<a href="#cg-item-16"><img src="https://picsum.photos/id/685/1200/1200" alt="Joshua Earle"></a>
</div>
<div id="cg-item-17" data-title="Lee Scott" style="--i:17; --bg-img:url(https://picsum.photos/id/699/1200/1200)">
<a href="#cg-item-17"><img src="https://picsum.photos/id/699/1200/1200" alt="Lee Scott"></a>
</div>
<div id="cg-item-18" data-title="Biegun Wschodni" style="--i:18; --bg-img:url(https://picsum.photos/id/611/1200/1200)">
<a href="#cg-item-18"><img src="https://picsum.photos/id/611/1200/1200" alt="Biegun Wschodni"></a>
</div>
<div id="cg-item-19" data-title="Drew Geraets" style="--i:19; --bg-img:url(https://picsum.photos/id/480/1200/1200)">
<a href="#cg-item-19"><img src="https://picsum.photos/id/480/1200/1200" alt="Drew Geraets"></a>
</div>
<div id="cg-item-20" data-title="Julia Caesar" style="--i:20; --bg-img:url(https://picsum.photos/id/773/1200/1200)">
<a href="#cg-item-20"><img src="https://picsum.photos/id/773/1200/1200" alt="Julia Caesar"></a>
</div>
<h1>Circular Gallery</h1>
</section>Sizes
Small
<section class="art-circular-gallery art-circular-gallery-sm">
<!-- 20 items with --i:1 through --i:20 -->
<div id="cg-sm-1" data-title="Yoni Kaplan-Nadel" style="--i:1; --bg-img:url(https://picsum.photos/id/27/1200/1200)">
<a href="#cg-sm-1"><img src="https://picsum.photos/id/27/1200/1200" alt="Yoni Kaplan-Nadel"></a>
</div>
<!-- ... repeat for items 2–20 ... -->
<h1>Circular Gallery</h1>
</section>Large
<section class="art-circular-gallery art-circular-gallery-lg">
<!-- 20 items with --i:1 through --i:20 -->
<div id="cg-lg-1" data-title="Yoni Kaplan-Nadel" style="--i:1; --bg-img:url(https://picsum.photos/id/27/1200/1200)">
<a href="#cg-lg-1"><img src="https://picsum.photos/id/27/1200/1200" alt="Yoni Kaplan-Nadel"></a>
</div>
<!-- ... repeat for items 2–20 ... -->
<h1>Circular Gallery</h1>
</section>CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--art-circular-gallery-size | 600px | Overall container size; all other dimensions scale from this |
--art-circular-gallery-radius | calc(size * 0.42) | Radius of the circle on which cards are placed |
--art-circular-gallery-card-width | calc(size * 0.07) | Width of each thumbnail card |
--art-circular-gallery-card-border-radius | calc(size * 0.012) | Border radius of thumbnail cards |
--art-circular-gallery-arc-size | 0.95 | Fraction of the circle arc used (gap between first and last card) |
--art-circular-gallery-arc-center | 0.275 | Arc midpoint position; controls where the cluster appears on the circle |
--art-circular-gallery-arc-shift-delta | 0.0075 | Per-unit spread amount when neighbors respond to hover |
--art-circular-gallery-card-trans-duration | 1000ms | Duration of the card position transition |
--art-circular-gallery-rotation | 0deg | Overall rotation of the gallery (@property typed, animatable) |
Per-item inline style properties:
| Property | Description |
|---|---|
--i | Card index (1–20); determines position on the circle arc |
--bg-img | CSS url() of the image shown in the full-size center preview |
HTML Structure
<section class="art-circular-gallery">
<div id="item-1" data-title="Photographer Name"
style="--i:1; --bg-img:url(https://example.com/photo.jpg)">
<a href="#item-1">
<img src="https://example.com/photo.jpg" alt="Photographer Name">
</a>
</div>
<!-- repeat for items 2 through 20 -->
<h1>Circular Gallery</h1>
</section>
Each div carries:
id— used as the:targetanchor; clicking thea href="#item-N"activates itdata-title— shown as a tooltip viacontent: attr(data-title)on::after--i— integer 1–20 that controls where the card sits on the arc--bg-img— the full-size image shown in the circular center preview
The h1 must be the last child so it sits at the center and does not consume an --i slot.
API Reference
| Class | Description |
|---|---|
art-circular-gallery | Root element — defines the circle layout, all tokens, and hover-spread selectors |
art-circular-gallery-sm | Small size (--art-circular-gallery-size: 400px) |
art-circular-gallery-lg | Large size (--art-circular-gallery-size: 800px) |
CSS
@property --art-circular-gallery-rotation {
syntax: "<angle>";
inherits: true;
initial-value: 0deg;
}
@layer css-art {
.art-circular-gallery {
--art-circular-gallery-size: 600px;
--art-circular-gallery-radius: calc(var(--art-circular-gallery-size) * 0.42);
--art-circular-gallery-card-width: calc(var(--art-circular-gallery-size) * 0.09);
--art-circular-gallery-card-border-radius: calc(var(--art-circular-gallery-size) * 0.012);
--art-circular-gallery-arc-size: 0.95;
--art-circular-gallery-arc-center: 0.275;
--art-circular-gallery-arc-start: calc(
var(--art-circular-gallery-arc-center) - var(--art-circular-gallery-arc-size) / 2
);
--art-circular-gallery-arc-shift-delta: 0.0075;
--art-circular-gallery-card-trans-duration: 1000ms;
--art-circular-gallery-card-trans-easing: linear(
0, 0.01 0.8%, 0.038 1.6%, 0.154 3.4%, 0.781 9.7%, 1.01 12.5%,
1.089 13.8%, 1.153 15.2%, 1.195 16.6%, 1.219 18%, 1.224 19.7%,
1.208 21.6%, 1.172 23.6%, 1.057 28.6%, 1.007 31.2%, 0.969 34.1%,
0.951 37.1%, 0.953 40.9%, 0.998 50.4%, 1.011 56%, 0.998 74.7%, 1
);
anchor-name: --art-circular-gallery-center;
transform: rotate(var(--art-circular-gallery-rotation));
transition: transform 700ms linear;
position: relative;
width: var(--art-circular-gallery-size);
height: var(--art-circular-gallery-size);
aspect-ratio: 1;
display: grid;
place-content: center;
font-family: sans-serif;
h1 {
margin: 0;
text-align: center;
font-size: clamp(1rem, calc(var(--art-circular-gallery-size) * 0.035), 2.5rem);
font-weight: 300;
letter-spacing: 0.05em;
transform: rotate(calc(var(--art-circular-gallery-rotation) * -1));
}
div {
--arc-step: calc(var(--art-circular-gallery-arc-size) / 19);
--arc-shift: calc(var(--art-circular-gallery-arc-shift-delta) * var(--d, 1));
--card-offset-distance: calc(
(var(--art-circular-gallery-arc-start)
+ (var(--i) - 1) * var(--arc-step)
+ var(--arc-shift)) * 100%
);
&:has(a:hover), &:target { --title-opacity: 1; }
&:target {
--zindex: 2;
--center-opacity: 1;
--center-scale: 1;
--image-outline-color: rgba(255 255 255 / 0.5);
}
&::before {
content: "";
position: fixed;
position-anchor: --art-circular-gallery-center;
position-area: span-all;
transform: rotate(calc(var(--art-circular-gallery-rotation) * -1));
width: calc(var(--art-circular-gallery-size) * 0.4);
aspect-ratio: 1;
border-radius: 9in;
background: var(--bg-img);
background-size: cover;
background-repeat: no-repeat;
outline: 10px solid rgba(255 255 255 / 0.3);
outline-offset: -10px;
opacity: var(--center-opacity, 0);
scale: var(--center-scale, 0);
pointer-events: none;
transition-property: scale, opacity;
transition-duration: 300ms, 150ms;
}
&::after {
content: attr(data-title);
position: fixed;
position-anchor: --art-circular-gallery-card;
position-area: center span-all;
z-index: 2;
pointer-events: none;
opacity: var(--title-opacity, 0);
transition: opacity 150ms;
border: 1px solid rgb(255 255 255 / 0.75);
background: rgb(0 0 0 / 0.25);
backdrop-filter: blur(3px);
padding: 0.25em 1em;
border-radius: 5px;
font-size: clamp(0.55rem, calc(var(--art-circular-gallery-size) * 0.012), 0.85rem);
white-space: nowrap;
@starting-style { opacity: 0; }
}
a {
position: absolute;
display: block;
width: var(--art-circular-gallery-card-width);
aspect-ratio: 4 / 6;
border-radius: var(--art-circular-gallery-card-border-radius);
border: 2px solid rgba(255 255 255 / 0.6);
offset-path: circle(var(--art-circular-gallery-radius) at 50% 50%);
offset-distance: var(--card-offset-distance);
offset-rotate: auto;
offset-anchor: 50% 0%;
transition:
offset var(--art-circular-gallery-card-trans-duration)
var(--art-circular-gallery-card-trans-easing),
scale 200ms ease;
z-index: var(--zindex, 1);
anchor-name: --art-circular-gallery-card;
&:hover {
scale: 2.5;
z-index: 3;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: inherit;
outline: 5px solid var(--image-outline-color, transparent);
outline-offset: -5px;
transition: outline 300ms ease-in-out;
}
}
}
}
/* Forward sibling spread (+1 to +4) */
.art-circular-gallery:has(> *:hover) > *:hover + *,
.art-circular-gallery:has(> :last-child:hover) > :nth-child(1) { --d: 3; }
.art-circular-gallery:has(> *:hover) > *:hover + * + *,
.art-circular-gallery:has(> :last-child:hover) > :nth-child(2),
.art-circular-gallery:has(> :nth-last-child(2):hover) > :nth-child(1) { --d: 2; }
.art-circular-gallery:has(> *:hover) > *:hover + * + * + *,
.art-circular-gallery:has(> :last-child:hover) > :nth-child(3),
.art-circular-gallery:has(> :nth-last-child(2):hover) > :nth-child(2),
.art-circular-gallery:has(> :nth-last-child(3):hover) > :nth-child(1) { --d: 1; }
.art-circular-gallery:has(> *:hover) > *:hover + * + * + * + *,
.art-circular-gallery:has(> :last-child:hover) > :nth-child(4),
.art-circular-gallery:has(> :nth-last-child(2):hover) > :nth-child(3),
.art-circular-gallery:has(> :nth-last-child(3):hover) > :nth-child(2),
.art-circular-gallery:has(> :nth-last-child(4):hover) > :nth-child(1) { --d: 0.5; }
/* Backward sibling spread (-1 to -4) */
.art-circular-gallery *:has(+ *:hover),
.art-circular-gallery:has(> :nth-child(1):hover) :nth-last-child(1) { --d: -3; }
.art-circular-gallery *:has(+ * + *:hover),
.art-circular-gallery:has(> :nth-child(1):hover) :nth-last-child(2),
.art-circular-gallery:has(> :nth-child(2):hover) :nth-last-child(1) { --d: -2; }
.art-circular-gallery *:has(+ * + * + *:hover),
.art-circular-gallery:has(> :nth-child(1):hover) :nth-last-child(3),
.art-circular-gallery:has(> :nth-child(2):hover) :nth-last-child(2),
.art-circular-gallery:has(> :nth-child(3):hover) :nth-last-child(1) { --d: -1; }
.art-circular-gallery *:has(+ * + * + * + *:hover),
.art-circular-gallery:has(> :nth-child(1):hover) :nth-last-child(4),
.art-circular-gallery:has(> :nth-child(2):hover) :nth-last-child(3),
.art-circular-gallery:has(> :nth-child(3):hover) :nth-last-child(2),
.art-circular-gallery:has(> :nth-child(4):hover) :nth-last-child(1) { --d: -0.5; }
.art-circular-gallery-sm { --art-circular-gallery-size: 400px; }
.art-circular-gallery-lg { --art-circular-gallery-size: 800px; }
}