An inventory of rounded photographs that barely overlap one another is a traditional net design sample.
You’re for certain questioning what the novelty we’re bringing right here is, proper? It has been accomplished numerous instances.
You’re proper. The principle concept will not be complicated, however the brand new factor is the responsive half. We are going to see the best way to dynamically modify 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 best way!
Here’s a demo of what we’re creating. You’ll be able to 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 presently restricted to Chrome and Edge, however will work in different browsers because the sibling-index() and sibling-count() features acquire broader help. You’ll be able to observe Firefox help in Ticket #1953973 and WebKit’s place in Situation #471.
We’ll get even deeper into issues in a second article. For now, let’s re-create this demo!
Responsive Checklist of Avatars Utilizing Trendy CSS
- Horizontal Lists (You’re right here!)
- Round Lists (Coming this week)
The preliminary setup
We begin with the HTML, which is a set of picture parts 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 are able to make the photographs circles with border-radius and squish them shut along with somewhat unfavorable 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 — however it’s additionally tougher to code for the reason that cut-out wants to think about the following (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 photographs — a radius equal to 50% in each instructions — and its heart level would be the midpoint of the following factor (calc(150% - 20px)). With out the overlap, the middle of the following factor is at 50% (heart of the particular factor) + 100%. However as a result of overlap, the following picture is nearer, so we scale back the gap by 20px, which is the worth utilized by the margin. This minimize the picture from the precise facet.
If we would like the cut-out on the left facet, we transfer the circle within the different course: 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 for 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 regulate the picture measurement and hole between the photographs.
.container {
--s: 120px; /* picture measurement*/
--g: 10px; /* the hole */
show: flex;
}
.container img {
width: var(--s);
border-radius: 50%;
margin-right: -20px;
/* Reduce-out on the precise facet */
masks: radial-gradient(50% 50% at calc(150% - 20px),
#0000 calc(100% + var(--g)),#000);
}
/* Reduce-out on the left facet */
.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 course of the cut-out from proper (the default) to left as an alternative.
What now we have is already good. It really works tremendous and you need to use it, however it could possibly be extra interactive. The overlap look good however wouldn’t be higher if we may enlarge it on smaller screens to assist preserve house, or even perhaps take away it altogether on bigger screens the place there’s loads of room to indicate the complete photographs?
Let’s make this extra interactive and responsive.
The responsive half
Let’s think about the entire measurement of the photographs exceeds the scale of the .container. That ends in an overflow, so we have to assign a unfavorable margin to every picture to soak up that house and guarantee all the photographs match within the container.
It appears like we'd like some JavaScript to calculate the surplus of house after which divide it by the variety of photographs to get the margin worth. And possibly put this logic inside a resize listener in case the container change its measurement.
I'm kidding, in fact! We are able to remedy this utilizing trendy CSS that's small and maintainable.
If we had been to precise what we'd like mathematically, the components of the margin needs to be equal to:
margin-right: (size_of_container - N x size_of_image)/(N - 1);
…the place N is the variety of photographs, 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 measurement (--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 unravel for is N, the variety of photographs. We may use a inflexible magic quantity right here, say 10, however what if we would like fewer or extra photographs within the container? We’d should replace the CSS every time. We would like an answer that adapts to no matter variety of photographs we throw at it.
That’s the place the brand new sibling-count() perform is available in actual useful. It’s going to be the very best strategy transferring ahead because it routinely calculates the variety of little one parts throughout the container. So, if there are 10 photographs 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 under and see how the photographs behave. Once more, sibling-count() help is proscribed for the time being, however you possibly can test it out within the newest Chrome or Safari Know-how Preview.
It’s fairly good! The pictures routinely modify to slot in the container, however we are able to nonetheless enhance this barely. When the container measurement is massive sufficient, the calculated worth of margin can be constructive and we get massive areas between the photographs. You in all probability wish to maintain that conduct, however in my case, I need the picture to stay as shut as attainable.
To do that, we are able to set a most boundary to the margin worth and ensure it doesn’t get any greater than 0:
margin-right: min((100% - sibling-count() * var(--s))/(sibling-count() - 1), 0px);
We are able to additionally re-use the the hole variable (--g) to take care of an area between objects:
margin-right: min((100% - sibling-count() * var(--s))/(sibling-count() - 1), var(--g));
In case you’re questioning why I'm utilizing the min() perform 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! We've the identical code because the earlier part, however as an alternative of the arbitrary 20px worth, we used the final components.
.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 difficulty is? Assume a second about it as a result of it’s one thing chances are you'll face in different conditions.
It’s associated to percentages. With margin, the share refers back to the container measurement, however inside masks, it considers one other reference, which suggests the values aren’t equal. We have to retrieve the container measurement in another 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 format problem:
Tada! The place and the masks modify completely when the container is resized.
The animation half
The concept of the animation is to totally reveal a picture on hover if there may be an overlap between objects, like this:
How will 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 really would possibly need a little little bit of margin between photographs, 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 checklist (the row with the cut-out on the left) is not so good as the highest checklist as a result of the masks is a bit off on hover.
Let’s first repair the masks earlier than tackling the overflow.
The problem is that I'm utilizing margin-right for the spacing whereas the cut-out impact is on the left. It really works tremendous once we don’t want any animation however as you possibly can see, it’s not fairly good within the final demo. We'd like 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 precise, 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 a lot better and respects each the left and proper sides:
Let’s repair the overflow now. Bear in mind the earlier components the place we cut up the surplus of house throughout N - 1 parts?
(size_of_container - N x size_of_image)/(N - 1)
Now we have to exclude yet another factor within the equation, which suggests we change the N with N - 1 and change the N - 1 with N - 2:
(size_of_container - (N - 1) x size_of_image)/(N - 2)
Nonetheless, that additional excluded factor nonetheless takes up house contained in the container. We have to account for its measurement and subtract it from the container measurement:
((size_of_container - (size_of_image + hole)) - (N - 1) x size_of_image)/(N - 2)
I'm contemplating the scale 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 needs to be utilized on all the photographs when one picture is hovered (besides the hovered picture). It is a nice alternative to write down a elaborate selector utilizing :has() and :not()!
/* Choose photographs that aren't hovered when the container accommodates a hovered picture */
.container:has(:hover) img:not(:hover) {
/**/
}
And we plug the components into that:
.container:has(:hover) img:not(:hover) {
--_m: min((100cqw - var(--g) - sibling-count()*var(--s))/(sibling-count() - 2), var(--g));
}
Verify that out — no extra overflow on hover in each instructions! All we're lacking now's the precise animation that easily transitions the spacing slightly than snapping issues into place. All we'd like is so as to add somewhat transition on the --_m variable:
transition: --_m .3s linear;
If we do this, nonetheless, 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 clean 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 checklist and the primary one within the backside checklist 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 modify the earlier selector like under:
.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 limit the choice to the weather that aren't :last-child for the primary checklist and never :first-child for the second checklist. 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 traditional element, however the true aim was to be taught a number of CSS methods and depend on new options that you'll undoubtedly want in different conditions.
Within the subsequent article, we’ll add extra complexity and canopy much more trendy CSS for a fair extra satisfying sample! Keep tuned.
Responsive Checklist of Avatars Utilizing Trendy CSS
- Horizontal Lists (You're right here!)
- Round Lists (Coming this week)
