Prepared for the second half? If you happen to recall, final time we labored on a responsive record of overlapping avatar photographs that includes a cut-out between them.

We’re nonetheless making a responsive record of avatars, however this time it is going to be a round record.

This design is much less widespread than the horizontal record, but it surely’s nonetheless an excellent train to discover new CSS tips.
Let’s begin with a demo. You may resize it and see how the pictures behave, and in addition hover them to get a cool reveal impact.
The next demo is at present restricted to Chrome and Edge, however will work in different browsers because the sibling-index() and sibling-count() capabilities acquire broader help. You may monitor Firefox help in Ticket #1953973 and WebKit’s place in Subject #471.
We are going to depend on the identical HTML construction and CSS base as the instance we lined in Half 1: an inventory of photographs inside a container with masks-ed cutouts. This time, nonetheless, the positions will likely be totally different.
Responsive Record of Avatars Utilizing Trendy CSS
- Horizontal Lists
- Round Lists (You might be right here!)
Putting Pictures Round a Circle
There are a number of strategies for putting photographs round a circle. I’ll begin with my favourite one, which is much less recognized however makes use of a easy code that depends on the CSS offset property.
.container {
show: grid;
}
.container img {
grid-area: 1/1;
offset: circle(180px) calc(100%*sibling-index()/sibling-count()) 0deg;
}
The code doesn’t look tremendous intuitive, however its logic is pretty simple. The offset property is a shorthand, so let’s write it the longhand approach to see how breaks down:
offset-path: circle(180px);
offset-distance: calc(100%*sibling-index()/sibling-count());
offset-rotate: 0deg;
We outline a path to be a circle with a radius of 180px. All the pictures will “observe” that path, however will initially be on high of one another. We have to modify their distance to alter their place alongside the trail (i.e., the circle). That’s the place offset-distance comes into play, which we mix with the sibling-index() and sibling-count() capabilities to create code that works with any variety of parts as an alternative of working with actual numbers.
For six parts, the values will likely be as follows:
100% x 1/6 = 16.67%
100% x 2/6 = 33.33%
100% x 3/6 = 50%
100% x 4/6 = 66,67%
100% x 5/6 = 83.33%
100% x 6/6 = 100%
This can place the weather evenly across the circle. To this, we add a rotation equal to 0deg utilizing offset-rotate to maintain the weather straight in order that they don’t rotate as they observe the round path. From there, all we’ve got to do is replace the circle’s radius with the worth we would like.
That’s my most well-liked method, however there’s a second one which makes use of the rework property to mix two rotations with a translation:
.container {
show: grid;
}
.container img {
grid-area: 1/1;
--_i: calc(1turn*sibling-index()/sibling-count());
rework: rotate(calc(-1*var(--_i))) translate(180px) rotate(var(--_i));
}
The interpretation accommodates the circle radius worth and the rotations use generic code that depends on the sibling-* capabilities the identical manner we did with offset-distance.
Though I desire the primary method, I’ll depend on the second as a result of it permits me to reuse the rotation angle in additional locations.
The Responsive Half
Just like the horizontal responsive record from the final article, I’ll depend on container question models to outline the radius of the circle and make the element responsive.

.container {
--s: 120px; /* picture measurement */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: calc(50cqw - var(--s)/2);
--_i: calc(1turn*sibling-index()/sibling-count());
rework: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}
Resize the container within the demo beneath and see how the pictures behave:
It’s responsive, however when the container will get larger, the pictures are too unfold out, and I don’t like that. It will be good to maintain them as shut as doable. In different phrases, we contemplate the smallest circle that accommodates all the pictures with out overlap.
Bear in mind what we did within the first half: we added a most boundary to the margin for the same purpose. We are going to do the identical factor right here:
--_r: min(50cqw - var(--s)/2, R);
I do know you don’t desire a boring geometry lesson, so I’ll skip it and provide the worth of R:
S/(2 x sin(.5turn/N))
Written in CSS:
--_r: min(50cqw - var(--s)/2,var(--s)/(2*sin(.5turn/sibling-count())));
Now, if you make the container larger, the pictures will keep shut to one another, which is ideal:
Let’s introduce one other variable for the hole between photographs (--g) and replace the method barely to maintain a small hole between the pictures.
.container {
--s: 120px; /* picture measurement */
--g: 10px; /* the hole */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: min(50cqw - var(--s)/2,(var(--s) + var(--g))/(2*sin(.5turn/sibling-count())));
--_i: calc(1turn*sibling-index()/sibling-count());
rework: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}
The Reduce-Out Impact
For this half, we will likely be utilizing the identical masks that we used within the final article:
masks: radial-gradient(50% 50% at X Y, #0000 calc(100% + var(--g)), #000);
With the horizontal record, the values of X and Y have been fairly easy. We didn’t need to outline Y since its default worth did the job, and the X worth was both 150% + M or -50% - M, with M being the margin that controls the overlap. Seen in a different way, X and Y are the coordinates of the middle level of the following or earlier picture within the record.
That’s nonetheless the case this time round, however the worth is trickier to calculate:

The thought is to begin from the middle of the present picture (50% 50%) and transfer to the middle of the following picture (X and Y). I’ll first observe phase A to achieve the middle of the large circle after which observe phase B to achieve the middle of the following picture.
That is the method:
X = 50% - Ax + Bx
Y = 50% - Ay + By
Ax and Ay are the projections of the phase A on the X-axis and the Y-axis. We will use trigonometric capabilities to get the values.
Ax = r x sin(i);
Ay = r x cos(i);
The r represents the circle’s radius outlined by the CSS variable --_r, and i represents the angle of rotation outlined by the CSS variable --_i.
Similar logic with the B phase:
Bx = r x sin(j);
By = r x cos(j);
The j is just like i, however for the subsequent picture within the sequence, which means we increment the index by 1. That offers us the next CSS calculations for every variable:
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
And the ultimate code with the masks:
.container {
--s: 120px; /* picture measurement */
--g: 14px; /* the hole */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: min(50cqw - var(--s)/2,(var(--s) + var(--g))/(2*sin(.5turn/sibling-count())));
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
rework: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
masks: radial-gradient(50% 50% at
calc(50% + var(--_r)*(cos(var(--_j)) - cos(var(--_i))))
calc(50% + var(--_r)*(sin(var(--_i)) - sin(var(--_j)))),
#0000 calc(100% + var(--g)), #000);
}
Cool, proper? You would possibly discover two totally different implementations for the cut-out. The method I used beforehand thought of the following picture, but when we contemplate the earlier picture as an alternative, the cut-out goes in one other path. So, slightly than incrementing the index, we decrement as an alternative and assign it to a .reverse class that we will use after we need the cut-out to go in the other way:
.container img {
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
}
.container.reverse img {
--_j: calc(1turn*(sibling-index() - 1)/sibling-count());
}
The Animation Half
Just like what we did within the final article, the purpose of this animation is to take away the overlap when a picture is hovered to completely reveal it. Within the horizontal record, we merely set its margin property to 0, and we modify the margin of the opposite photographs to forestall overflow.
This time, the logic is totally different. We are going to rotate the entire photographs besides the hovered one till the hovered picture is totally seen. The path of the rotation will rely upon the cut-out path, after all.

To rotate the picture, we have to replace the --_i variable, which is used as an argument for the rotate perform. Let’s begin with an arbitrary worth for the rotation, say 20deg.
.container img {
--_i: calc(1turn*sibling-index()/sibling-count());
}
.container:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() + 20deg);
}
.container.reverse:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() - 20deg);
}
Now, when a picture is hovered, all of photographs rotate by 20deg. Strive it out within the following demo.
Hmm, the pictures do certainly rotate, however the masks is just not following alongside. Don’t overlook that the masks considers the place of the following or earlier picture outlined by --_j and the following/earlier picture is rotating — therefore we have to additionally replace the --_j variable when the hover occurs.
.container img {
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
}
.container.reverse img {
--_j: calc(1turn*(sibling-index() - 1)/sibling-count());
}
.container:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() + 20deg);
--_j: calc(1turn*(sibling-index() + 1)/sibling-count() + 20deg);
}
.container.reverse:has(:hover) img {
--_i: calc(1turn*sibling-index()/sibling-count() - 20deg);
--_j: calc(1turn*(sibling-index() - 1)/sibling-count() - 20deg);
}
That’s loads of redundant code. Let’s optimize it just a little by defining further variables:
.container img {
--_a: 20deg;
--_i: calc(1turn*sibling-index()/sibling-count() + var(--_ii, 0deg));
--_j: calc(1turn*(sibling-index() + 1)/sibling-count() + var(--_jj, 0deg));
}
.container.reverse img {
--_i: calc(1turn*sibling-index()/sibling-count() - var(--_ii, 0deg));
--_j: calc(1turn*(sibling-index() - 1)/sibling-count() - var(--_jj, 0deg));
}
.container:has(:hover) img {
--_ii: var(--_a);
--_jj: var(--_a);
}
Now the angle (--_a) is outlined in a single place, and I contemplate two intermediate variables so as to add an offset to the --_i and --_j variables.
The rotation of all the pictures is now good. Let’s disable the rotation of the hovered picture:
.container img:hover {
--_ii: 0deg;
--_jj: 0deg;
}
Oops, the masks is off once more! Do you see the problem?
We need to cease the hovered picture from rotating whereas permitting the remainder of the pictures to rotate. Due to this fact, the --_j variable of the hovered picture must replace because it’s linked to the following or earlier picture. So we must always take away --_jj: 0deg and preserve solely --_ii: 0deg.
.container img:hover {
--_ii: 0deg;
}
That’s just a little higher. We mounted the cut-out impact on the hovered picture, however the general impact remains to be not good. Let’s not overlook that the hovered picture is both the following or earlier picture of one other picture, and because it’s not rotating, one other --_j variable wants to stay unchanged.
For the primary record, it’s the variable of the earlier picture that ought to stay unchanged. For the second record, it’s the variable of the following picture:
/* choose earlier component of hovered */
.container:not(.reverse) img:has(+ :hover),
/* choose subsequent component of hovered */
.container.reverse img:hover + * {
--_jj: 0deg;
}
In case you might be questioning how I knew to do that, nicely, I attempted each methods and I picked the one which labored. It was both the code above or this:
.container:not(.reverse) img:hover + *,
.container.reverse img:has(+ :hover) {
--_jj: 0deg;
}
We’re getting nearer! All the pictures behave appropriately apart from one in every record. Strive hovering all of them to establish the perpetrator.
Can you determine what we’re lacking? Suppose a second about it.
Our record is round, however the HTML code is just not, so even when the primary and final photographs are visually positioned subsequent to one another, within the code, they don’t seem to be. We can not hyperlink each of them utilizing the adjoining sibling selector (+). We want two extra selectors to cowl these edge circumstances:
.container.reverse:has(:last-child:hover) img:first-child,
.container:not(.reverse):has(:first-child:hover) img:last-child {
--_jj: 0deg;
}
Oof! We’ve mounted all the problems, and now our hover impact is nice, but it surely’s nonetheless not good. Now, as an alternative of utilizing an arbitrary worth for the rotation, we must be correct. We’ve to search out the smallest worth that removes the overlap whereas protecting the pictures as shut as doable.

We will get the worth with some trigonometry. I’ll skip the geometry lesson once more (we’ve got sufficient complications as it’s!) and provide the worth:
--_a: calc(2*asin((var(--s) + var(--g))/(2*var(--_r))) - 1turn/sibling-count());
Now we will say every little thing is ideal!
Conclusion
This one was a bit robust, proper? Don’t fear should you obtained a bit misplaced with all of the complicated formulation. They’re very particular to this instance, so even in case you have already overlook about them, that’s tremendous. The purpose was to discover some fashionable options and some CSS tips akin to offset, masks, sibling-* capabilities, container question models, min()/max(), and extra!
Responsive Record of Avatars Utilizing Trendy CSS
- Horizontal Lists
- Round Lists (You might be right here!)


![Why inventive groups want the security to fail [according to a senior director for Magic: The Gathering]](https://blog.aimactgrow.com/wp-content/uploads/2025/10/alicia-mickes-mim-header.webp-120x86.webp)






