An inventory of rounded photos that barely overlap one another is a basic internet design sample.

You might be for positive questioning what the novelty we’re bringing right here is, proper? It has been performed numerous occasions.
You might be proper. The principle concept just isn’t complicated, however the brand new factor is the responsive half. We’ll see the best way to dynamically regulate the overlap between the photographs to allow them to match inside their container. And we are going to make some cool animations for it alongside the way in which!
Here’s a demo of what we’re creating. You may resize the window and hover the photographs to see how they behave. And sure, the hole between the photographs is clear!
The next demo is at the moment restricted to Chrome and Edge, however will work in different browsers because the sibling-index() and sibling-count() capabilities achieve broader help. You may observe Firefox help in Ticket #1953973 and WebKit’s place in Difficulty #471.
We’ll get even deeper into issues in a second article. For now, let’s re-create this demo!
Responsive Listing of Avatars Utilizing Trendy CSS
- Horizontal Lists (You might be right here!)
- Round Lists
The preliminary setup
We begin with the HTML, which is a set of picture components in a dad or mum container:
Declaring flexbox on the container is all we have to line the photographs up in a single row:
.container {
show: flex;
}
We will make the photographs circles with border-radius and squish them shut along with somewhat destructive margin:
.container img {
border-radius: 50%;
margin-right: -20px;
}
.container img:last-child {
margin: 0;
}
Nothing fancy thus far. I'm utilizing an arbitrary worth for the margin to create an overlap:
The cut-out impact
We’ll want the masks property to chop the photographs and create the clear hole between them. Making the hole clear is essential right here because it makes the element look higher — but it surely’s additionally more difficult to code for the reason that cut-out wants to contemplate the subsequent (or earlier) factor in a method that stops one picture from obscuring the opposite.
masks: radial-gradient(50% 50% at calc(150% - 20px), #0000 100%, #000);
This masks creates a round form with the identical dimensions as one of many photos — a radius equal to 50% in each instructions — and its middle level would be the midpoint of the subsequent factor (calc(150% - 20px)). With out the overlap, the middle of the subsequent factor is at 50% (middle of the particular factor) + 100%. However as a result of overlap, the subsequent picture is nearer, so we scale back the space by 20px, which is the worth utilized by the margin. This reduce the picture from the appropriate aspect.
If we would like the cut-out on the left aspect, we transfer the circle within the different route: 50% - 100% + 20px.
Drag the slider within the subsequent demo for a visualization of how this works in each instructions. I'm eradicating the border-radius from the middle picture as an example the round form.
We apply this to all the photographs, and we're good to go. Discover that I'm utilizing a few CSS variables to manage the picture dimension and hole between the photographs.
.container {
--s: 120px; /* picture dimension*/
--g: 10px; /* the hole */
show: flex;
}
.container img {
width: var(--s);
border-radius: 50%;
margin-right: -20px;
/* Lower-out on the appropriate aspect */
masks: radial-gradient(50% 50% at calc(150% - 20px),
#0000 calc(100% + var(--g)),#000);
}
/* Lower-out on the left aspect */
.container.reverse img {
masks: radial-gradient(50% 50% at calc(-50% + 20px),
#0000 calc(100% + var(--g)),#000);
}
.container img:last-child {
margin: 0;
}
.container.reverse img:first-child,
.container:not(.reverse) img:last-child {
masks: none;
}
Pay extra consideration to the .reverse class. It switches the route of the cut-out from proper (the default) to left as an alternative.
What we've is already good. It really works advantageous and you should use it, but it surely might be extra interactive. The overlap look good however wouldn’t be higher if we may enlarge it on smaller screens to assist preserve area, or even perhaps take away it altogether on bigger screens the place there’s loads of room to point out the total photos?
Let’s make this extra interactive and responsive.
The responsive half
Let’s think about the full dimension of the photographs exceeds the dimensions of the .container. That ends in an overflow, so we have to assign a destructive margin to every picture to soak up that area and guarantee all the photographs match within the container.
It appears to be like like we want some JavaScript to calculate the surplus of area after which divide it by the variety of photos to get the margin worth. And possibly put this logic inside a resize listener in case the container change its dimension.
I'm kidding, after all! We will resolve this utilizing trendy CSS that's small and maintainable.
If we have been to precise what we want mathematically, the method of the margin must be equal to:
margin-right: (size_of_container - N x size_of_image)/(N - 1);
…the place N is the variety of photos, and we're dividing by N - 1 as a result of the final picture doesn’t want a margin. We have already got a variable for the picture dimension (--s) and we all know that the width of the container is 100%:
margin-right: (100% - N x var(--s))/(N - 1);
What want to resolve for is N, the variety of photos. We may use a inflexible magic quantity right here, say 10, however what if we would like fewer or extra photos within the container? We’d should replace the CSS every time. We would like an answer that adapts to no matter variety of photos we throw at it.
That’s the place the brand new sibling-count() operate is available in actual useful. It’s going to be the most effective strategy shifting ahead because it robotically calculates the variety of little one components inside the container. So, if there are 10 photos within the .container, the sibling-count() is 10.
margin-right: calc((100% - sibling-count() * var(--s))/(sibling-count() - 1));
Resize the container within the demo beneath and see how the photographs behave. Once more, sibling-count() help is proscribed in the mean time, however you may test it out within the newest Chrome or Safari Know-how Preview.
It’s fairly good! The pictures robotically regulate to slot in the container, however we are able to nonetheless enhance this barely. When the container dimension is giant sufficient, the calculated worth of margin shall be constructive and we get large areas between the photographs. You most likely wish to maintain that habits, however in my case, I need the picture to stay as shut as potential.
To do that, we are able to set a most boundary to the margin worth and ensure it doesn’t get any larger than 0:
margin-right: min((100% - sibling-count() * var(--s))/(sibling-count() - 1), 0px);
We will additionally re-use the the hole variable (--g) to keep up an area between gadgets:
margin-right: min((100% - sibling-count() * var(--s))/(sibling-count() - 1), var(--g));
For those who’re questioning why I'm utilizing the min() operate to outline a max boundary, learn this for an in depth rationalization. Briefly: you’re successfully setting a most with min() and a minimal with max().
The responsive half is ideal now!!
What we’re lacking is the cut-out impact we made with masks. For that, we are able to re-use the identical margin worth contained in the masks.

Oops, the photographs disappeared! Now we have the identical code because the earlier part, however as an alternative of the arbitrary 20px worth, we used the final method.
.container img {
--_m: min((100% - sibling-count() * var(--s))/(sibling-count() - 1), var(--g));
margin-right: var(--_m);
masks: radial-gradient(50% 50% at calc(150% + var(--_m)),
#0000 calc(100% + var(--g)),#000);
}
Are you able to guess what the problem is? Assume a second about it as a result of it’s one thing you could face in different conditions.
It’s associated to percentages. With margin, the proportion refers back to the container dimension, however inside masks, it considers one other reference, which implies the values aren’t equal. We have to retrieve the container dimension in a different way, utilizing container question items as an alternative.
First, we register the .container as a CSS “container”:
.container {
container-type: inline-size;
}
Then, we are able to say that the container’s width is 100cqi (or 100cqw) as an alternative of 100%, which fixes the structure challenge:
Tada! The place and the masks regulate completely when the container is resized.
The animation half
The thought of the animation is to totally reveal a picture on hover if there's an overlap between gadgets, like this:
How can we take away the overlap? All we do is replace the variable (--_m) we outlined earlier to zero when a picture is hovered:
.container img:hover {
--_m: 0px;
}
That takes out the margin and removes the cut-out impact as effectively. We truly would possibly desire a little little bit of margin between photos, so let’s make --_m equal to the hole (--g) as an alternative:
.container img:hover {
--_m: var(--g);
}
Not dangerous! However we are able to do higher. Discover how pushing one picture away from one other causes a picture on the finish to overflow the container. The underside listing (the row with the cut-out on the left) is inferior to the highest listing as a result of the masks is a bit off on hover.
Let’s first repair the masks earlier than tackling the overflow.
The difficulty is that I'm utilizing margin-right for the spacing whereas the cut-out impact is on the left. It really works advantageous after we don’t want any animation however as you may see, it’s not fairly good within the final demo. We want change to a margin-left as an alternative on the underside row. In different phrases, we use margin-right when the cut-out is on the appropriate, and margin-left when the cut-out is on the left.
.container:not(.reverse) img {
masks: radial-gradient(50% 50% at calc(150% + var(--_m)),
#0000 calc(100% + var(--g)), #000);
margin-right: var(--_m);
}
.container.reverse img {
masks: radial-gradient(50% 50% at calc(-50% - var(--_m)),
#0000 calc(100% + var(--g)), #000);
margin-left: var(--_m);
}
.container:not(.reverse) img:last-child,
.container.reverse img:first-child {
masks: none;
margin: 0;
}
Nice, now the cut-out impact is significantly better and respects each the left and proper sides:
Let’s repair the overflow now. Bear in mind the earlier method the place we cut up the surplus of area throughout N - 1 components?
(size_of_container - N x size_of_image)/(N - 1)
Now we have to exclude yet one more factor within the equation, which implies we exchange the N with N - 1 and exchange the N - 1 with N - 2:
(size_of_container - (N - 1) x size_of_image)/(N - 2)
Nevertheless, that additional excluded factor nonetheless takes up area contained in the container. We have to account for its dimension and subtract it from the container dimension:
((size_of_container - (size_of_image + hole)) - (N - 1) x size_of_image)/(N - 2)
I'm contemplating the dimensions plus a niche as a result of a margin that is the same as the hole is ready on a hovered picture, which is extra spacing we have to take away.
We simplify a bit:
(size_of_container - hole - N x size_of_image)/(N - 2)
We all know the best way to translate this into CSS, however the place ought to we apply it?
It must be utilized on all the photographs when one picture is hovered (besides the hovered picture). It is a nice alternative to put in writing a elaborate selector utilizing :has() and :not()!
/* Choose photos that aren't hovered when the container incorporates a hovered picture */
.container:has(:hover) img:not(:hover) {
/**/
}
And we plug the method into that:
.container:has(:hover) img:not(:hover) {
--_m: min((100cqw - var(--g) - sibling-count()*var(--s))/(sibling-count() - 2), var(--g));
}
Examine that out — no extra overflow on hover in each instructions! All we're lacking now could be the precise animation that easily transitions the spacing reasonably than snapping issues into place. All we want is so as to add somewhat transition on the --_m variable:
transition: --_m .3s linear;
If we try this, nevertheless, the transition doesn’t occur. It’s as a result of CSS doesn’t acknowledge the calculated worth as a correct CSS size unit. For that, we have to formally register --_m as a customized property utilizing the @property at-rule:
@property --_m {
syntax: "";
inherits: true;
initial-value: 0px
}
There we go:
Cool, proper? Having a easy change for the masks and the place is kind of satisfying. We nonetheless want to repair a small edge case. The final factor within the high listing and the primary one within the backside listing don’t have any margin, and they're all the time absolutely seen, so we have to exclude them from the impact.
When hovering them, nothing ought to occur, so we are able to regulate the earlier selector like beneath:
.container:not(.reverse):has(:not(:last-child):hover) img:not(:hover),
.container.reverse:has(:not(:first-child):hover) img:not(:hover) {
--_m: min((100cqw - var(--g) - sibling-count()*var(--s))/(sibling-count() - 2),var(--g));
}
As a substitute of merely checking if the container has a hovered factor, we prohibit the choice to the weather that aren't :last-child for the primary listing and never :first-child for the second listing. One other cool selector utilizing trendy CSS!
Right here is the ultimate demo with all of the changes made:
Conclusion
I hope you loved this little exploration of some trendy CSS options. We re-created a basic element, however the true objective was to be taught a couple of CSS methods and depend on new options that you'll positively want in different conditions.
Within the subsequent article, we’ll add extra complexity and canopy much more trendy CSS for a good extra satisfying sample! Keep tuned.
Responsive Listing of Avatars Utilizing Trendy CSS
- Horizontal Lists (You might be right here!)
- Round Lists









