A client once saw a fancy “Who’s Online” widget on a high-end SaaS site and decided their WordPress team page needed the same look. They did not want a grid, they wanted a circular ring of faces that overlapped neatly. Younger and more impatient at the time, I reached straight for absolute positioning and hardcoded nth-child rotations for six items. It looked great, right up until the client added a seventh team member, then an eighth, and the whole ring collapsed.
The lesson was simple: hardcoded geometry is technical debt. For responsive circular avatar lists the browser has to do the math instead of you. That means leaving behind the horizontal layouts from the previous post and getting into trigonometry in modern CSS.
A better way to handle circular avatar lists
We used to calculate the X and Y coordinates of every image in a circle with JavaScript. The offset property covers that now. It is cleaner, though still a bit niche. For production code I lean on a double rotation trick with transform instead. It sounds counter-intuitive, but it holds up, and it pairs neatly with the new sibling-index() and sibling-count() functions.
Here is the base logic. However many avatars you drop into bbioon-avatar-ring, they distribute themselves evenly, and you never hand-write another nth-child calculation. Your future self will appreciate that the day a client hires five more people on a Friday afternoon.
.bbioon-avatar-ring {
display: grid;
--bbioon-s: 100px; /* avatar size */
aspect-ratio: 1;
container-type: inline-size;
}
.bbioon-avatar-ring img {
grid-area: 1/1;
width: var(--bbioon-s);
--bbioon-r: calc(50cqw - var(--bbioon-s) / 2);
--bbioon-angle: calc(1turn * sibling-index() / sibling-count());
/* The magic: rotate out, translate to the radius, then rotate back to stay upright */
transform: rotate(calc(-1 * var(--bbioon-angle)))
translate(var(--bbioon-r))
rotate(var(--bbioon-angle));
}
This builds on the original research over at CSS-Tricks. sibling-index() is what makes it work: the CSS knows exactly where each element sits in the list without any help from us.
The cut-out and masking problem
Placing the images is the easy half. The harder part is the cut-out effect, where each circle looks like it is biting into the one next to it. That needs a mask built from a radial-gradient, and the center of the mask has to sit at the center of the next image in the circle. The math gets hairy there.
I tried plain percentages first, which does not work. In a circular coordinate system the relative position of the “next” image changes depending on where you are in the ring, so you need sin() and cos() inside calc(). It feels like high school geometry because it is high school geometry, and once the formula is in place it stays put.
.bbioon-avatar-ring img {
--bbioon-next-angle: calc(1turn * (sibling-index() + 1) / sibling-count());
mask: radial-gradient(50% 50% at
calc(50% + var(--bbioon-r) * (cos(var(--bbioon-next-angle)) - cos(var(--bbioon-angle))))
calc(50% + var(--bbioon-r) * (sin(var(--bbioon-angle)) - sin(var(--bbioon-next-angle)))),
#0000 calc(100% + 10px), #000);
}
Browser support is the detail people miss. sibling-index() works in Chrome and Edge today and is still pending in the others, so on client projects I usually ship a grid fallback alongside it.
What this changes in practice
Modern CSS has effectively turned the browser into a geometry engine. Layouts like this no longer need a heavy JavaScript library. Container queries plus trig functions give you circular avatar lists that respond to their container and do not need editing every time the team grows.
CSS geometry gets fiddly fast. If you are tired of debugging someone else’s stylesheet and just want the site to look right on every screen, drop my team a line. We have most likely seen your exact problem before.