I recently had a client come to me with a “Meet the Team” page that was a total disaster on mobile. They wanted that classic look: a row of circular avatars, each slightly overlapping the next. On desktop, with 12 team members, it looked fine. But the moment you looked at it on a phone, the images either shrunk into tiny dots or caused a horizontal scroll that went on for miles. Total nightmare.
My first instinct—and I’m admitting this because we’ve all been there—was to just throw a handful of media queries at it. I figured I’d just manually adjust the negative margin for different breakpoints. “If screen width is less than 480px, set margin to -30px.” Yeah, that lasted about ten minutes until the client added a 13th team member. The whole layout shattered again. Hardcoding margins for dynamic content is a fool’s errand. There’s a better way to build modern CSS avatars that actually respond to their container.
The Math Behind Dynamic Overlap
To make this truly responsive, we need the CSS to “know” how many items are in the list and how much space it has to work with. Usually, this is where a dev would reach for a heavy JavaScript resize listener. But we can actually handle the heavy lifting with a simple math formula. Which builds on a great concept I saw over at [css-tricks.com]. The logic is simple: subtract the total width of all images from the container width, then divide that “excess” by the number of gaps between images.
.bbioon-avatar-container {
--s: 100px; /* Image size */
--g: 10px; /* The minimum gap */
display: flex;
container-type: inline-size;
}
.bbioon-avatar-container img {
width: var(--s);
border-radius: 50%;
/* The magic formula */
--_m: min((100cqw - sibling-count() * var(--s)) / (sibling-count() - 1), var(--g));
margin-right: var(--_m);
}
Here is the kicker: we’re using sibling-count(). This is a relatively new CSS function that gives us the total number of children in the container. By combining it with 100cqw (container query width), the avatars automatically squish together as the container gets smaller. No media queries required. Trust me on this, once you start using container units instead of percentages for internal layout logic, you’ll never go back.
Fixing the Mask with Container Queries
The overlap looks good, but to get that clean “cut-out” effect where the background shows through between images, we need a mask. I initially tried using percentage values inside the radial gradient, but they didn’t align properly because percentages inside a mask refer to the image itself, not the container. It was a mess. The fix? Use those same container query units again.
.bbioon-avatar-container img {
mask: radial-gradient(50% 50% at calc(150% + var(--_m)), #0000 calc(100% + var(--g)), #000);
}
@property --_m {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
By registering the --_m variable with @property, we can actually animate the transition. When a user hovers over an avatar, we can reset the margin to zero and the mask will smoothly slide out of the way. It’s a small detail, but it’s the kind of polish that makes a site feel high-end rather than just “functional.”
Stop Fighting the Browser
Modern CSS has finally reached a point where we don’t need to micromanage every pixel. Between :has() selectors and sibling-count(), we can build components that are truly aware of their environment. If you’re still writing 500 lines of media queries for a simple list of images, you’re working too hard.
Look, this stuff gets complicated fast when you’re dealing with browser support and edge cases. If you’re tired of debugging someone else’s mess and just want your site to work perfectly on every device, drop my team a line. We’ve probably seen it before.
Are you still using JavaScript to calculate your layout widths, or have you made the jump to container queries yet?
Leave a Reply