Prepared for the second half? In the event you recall, final time we labored on a responsive checklist of overlapping avatar photos that includes a cut-out between them.
We’re nonetheless making a responsive checklist of avatars, however this time will probably be a round checklist.

This design is much less frequent than the horizontal checklist, nevertheless it’s nonetheless a very good train to discover new CSS tips.
Let’s begin with a demo. You possibly can resize it and see how the photographs behave, and in addition hover them to get a cool reveal impact.
The next demo is presently restricted to Chrome and Edge, however will work in different browsers because the sibling-index() and sibling-count() features achieve broader help. You possibly can observe Firefox help in Ticket #1953973 and WebKit’s place in Subject #471.
We’ll depend on the identical HTML construction and CSS base as the instance we coated in Half 1: an inventory of photos inside a container with masks-ed cutouts. This time, nevertheless, the positions will probably be completely different.
Responsive Listing of Avatars Utilizing Trendy CSS
- Horizontal Lists
- Round Lists (You’re right here!)
Inserting Photos Round a Circle
There are a number of strategies for putting photos round a circle. I’ll begin with my favourite one, which is much less identified 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 solution 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 photographs will “observe” that path, however will initially be on prime of one another. We have to alter 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() features to create code that works with any variety of components as a substitute of working with precise numbers.
For six components, the values will probably 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 so 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 wish.
That’s my most well-liked strategy, however there’s a second one which makes use of the remodel property to mix two rotations with a translation:
.container {
show: grid;
}
.container img {
grid-area: 1/1;
--_i: calc(1turn*sibling-index()/sibling-count());
remodel: rotate(calc(-1*var(--_i))) translate(180px) rotate(var(--_i));
}
The interpretation incorporates the circle radius worth and the rotations use generic code that depends on the sibling-* features the identical method we did with offset-distance.
Regardless that I desire the primary strategy, 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 checklist 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 dimension */
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());
remodel: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}
Resize the container within the demo beneath and see how the photographs behave:
It’s responsive, however when the container will get greater, the photographs are too unfold out, and I don’t like that. It might be good to maintain them as shut as doable. In different phrases, we take into account the smallest circle that incorporates all the photographs with out overlap.
Keep in mind what we did within the first half: we added a most boundary to the margin for the same motive. We’ll do the identical factor right here:
--_r: min(50cqw - var(--s)/2, R);
I do know you don’t need 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, while you make the container greater, the photographs will keep shut to one another, which is ideal:
Let’s introduce one other variable for the hole between photos (--g) and replace the method barely to maintain a small hole between the photographs.
.container {
--s: 120px; /* picture dimension */
--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());
remodel: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}
The Minimize-Out Impact
For this half, we will probably 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 checklist, the values of X and Y had been fairly easy. We didn’t should 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 another way, X and Y are the coordinates of the middle level of the following or earlier picture within the checklist.
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 section A to succeed in the middle of the massive circle after which observe section B to succeed in 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 section A on the X-axis and the Y-axis. We are able to use trigonometric features 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.
Identical logic with the B section:
Bx = r x sin(j);
By = r x cos(j);
The j is just like i, however for the subsequent picture within the sequence, that 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 dimension */
--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());
remodel: 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 completely different implementations for the cut-out. The method I used beforehand thought-about the following picture, but when we take into account the earlier picture as a substitute, the cut-out goes in one other course. So, moderately than incrementing the index, we decrement as a substitute and assign it to a .reverse class that we will use after we need the cut-out to go in the wrong 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 aim of this animation is to take away the overlap when a picture is hovered to completely reveal it. Within the horizontal checklist, we merely set its margin property to 0, and we alter the margin of the opposite photos to stop overflow.
This time, the logic is completely different. We’ll rotate all the photos besides the hovered one till the hovered picture is absolutely seen. The course of the rotation will rely on the cut-out course, in fact.

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 photos rotate by 20deg. Strive it out within the following demo.
Hmm, the photographs do certainly rotate, however the masks just isn’t following alongside. Don’t neglect 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 somewhat by defining extra 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 take into account two intermediate variables so as to add an offset to the --_i and --_j variables.
The rotation of all the photographs is now excellent. 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 photographs 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 should always take away --_jj: 0deg and preserve solely --_ii: 0deg.
.container img:hover {
--_ii: 0deg;
}
That’s somewhat higher. We fastened the cut-out impact on the hovered picture, however the total impact continues to be not excellent. Let’s not neglect 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 checklist, it’s the variable of the earlier picture that ought to stay unchanged. For the second checklist, it’s the variable of the following picture:
/* choose earlier factor of hovered */
.container:not(.reverse) img:has(+ :hover),
/* choose subsequent factor 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 photographs behave appropriately apart from one in every checklist. Strive hovering all of them to determine the offender.
Can you determine what we’re lacking? Assume a second about it.
Our checklist is round, however the HTML code just isn’t, so even when the primary and final photos are visually positioned subsequent to one another, within the code, they don’t seem to be. We can’t hyperlink each of them utilizing the adjoining sibling selector (+). We’d like two extra selectors to cowl these edge instances:
.container.reverse:has(:last-child:hover) img:first-child,
.container:not(.reverse):has(:first-child:hover) img:last-child {
--_jj: 0deg;
}
Oof! We’ve got fastened all the problems, and now our hover impact is nice, nevertheless it’s nonetheless not excellent. Now, as a substitute of utilizing an arbitrary worth for the rotation, we must be correct. We’ve got to search out the smallest worth that removes the overlap whereas preserving the photographs as shut as doable.

We are able to 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 the whole lot is ideal!
Conclusion
This one was a bit powerful, proper? Don’t fear for those who received a bit misplaced with all of the complicated formulation. They’re very particular to this instance, so even if in case you have already neglect about them, that’s high quality. The aim was to discover some trendy options and some CSS tips comparable to offset, masks, sibling-* features, container question models, min()/max(), and extra!
Responsive Listing of Avatars Utilizing Trendy CSS
- Horizontal Lists
- Round Lists (You’re right here!)
