Responsive list of avatars with modern CSS (part 2) | CSS tricks

Responsive list of avatars with modern CSS (part 2) | CSS tricks

Ready for the second part? If you remember, last time we worked on a responsive list with overlapping avatar images with a cutout in between.

We are still working on creating a responsive list of avatars, but this time it will be a circular list.

Two examples of circular avatar images shown, arranged in a circle. The first example has eight images. The second example has six images.

This design is less common than the horizontal list, but it’s still a good exercise for discovering new CSS tricks.

Let’s start with a demo. You can resize and see how the images behave, and you can also move them with the mouse to get a cool reveal effect.

The following demo is currently limited to Chrome and Edge, but works in other browsers as well sibling-index() And sibling-count() Features receive broader support. You can track Firefox support in Card #1953973 and WebKit’s position within it Issue #471.

We’ll rely on the same HTML structure and CSS basics as the example we covered in part 1: a list of images in a container with mask-ed cutouts. However, this time the positions will be different.

Responsive list of avatars using modern CSS

  1. Horizontal frames
  2. Circular Lists (You are here!)

Place images around a circle

There are different techniques for placing images around a circle. I’ll start with my favorite one, which is less known but uses a simple code that relies on the CSS offset property.

.container {
  display: grid;
}
.container img {
  grid-area: 1/1;
  offset: circle(180px) calc(100%*sibling-index()/sibling-count()) 0deg;
}

The code doesn’t look super intuitive, but its logic is fairly simple. The offset property is an abbreviation, so let’s write it the long way to see how it works:

offset-path: circle(180px);
offset-distance: calc(100%*sibling-index()/sibling-count());
offset-rotate: 0deg;

We define a path as a circle with a radius of 180px. All images will “follow” that path, but will initially be on top of each other. We need to adjust their distance to change their position along the path (i.e. the circle). That’s true offset-distance comes into play, which we combine with the sibling-index() And sibling-count() functions to create code that works with an arbitrary number of elements instead of working with exact numbers.

For six elements the values ​​are 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 will place the elements evenly around the circle. To this we add a rotation equal to 0deg to use offset-rotate to keep the elements straight so they don’t rotate as they follow the circular path. From there we just need to update the radius of the circle with the desired value.

That’s my preferred approach, but there’s a second one that uses the transform property to combine two rotations with a translation:

.container {
  display: grid;
}
.container img {
  grid-area: 1/1;
  --_i: calc(1turn*sibling-index()/sibling-count());
  transform: rotate(calc(-1*var(--_i))) translate(180px) rotate(var(--_i));
}

The translation contains the circle radius value and the rotations use generic code that depends on the sibling-* works the same way we did offset-distance.

Even though I prefer the first approach, I will rely on the second because it allows me to reuse the rotation angle in more places.

The responsive part

As with the horizontal responsive list from the previous article, I will rely on container query units to define the radius of the circle and make the component responsive.

Diagram of eight circular avatar images arranged around a circle. A red dotted line indicates the size and radius of the larger circle.
.container {
  --s: 120px; /* image size */

  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());
  transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}

Resize the container in the demo below and see how the images behave:

It’s responsive, but as the container grows the images are too spread out, and I don’t like that. It would be good to keep them as close together as possible. In other words, we consider the smallest circle that contains all images without overlap.

Remember what we did in the first part: we added a maximum limit to the margin for a similar reason. We’ll do the same here:

--_r: min(50cqw - var(--s)/2, R);

I know you don’t want a boring geometry lesson, so I’ll skip this one and give you its value 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 bigger, the images will stay close together, which is perfect:

Let’s introduce another variable for the gap between images (--g) and update the formula slightly to keep a small gap between the images.

.container {
  --s: 120px; /* image size */
  --g: 10px;  /* the gap */

  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());
  transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}

The cutout effect

For this part, we’ll use the same mask we used in the previous article:

mask: radial-gradient(50% 50% at X Y, #0000 calc(100% + var(--g)), #000);

The horizontal list displays the values ​​of X And Y were quite simple. We didn’t have to define Y because the default value did the job, and the X value was neither 150% + M or -50% - Mof M being the margin that controls the overlap. Seen differently, X And Y are the coordinates of the center of the next or previous image in the list.

That is still the case this time, but the value is more difficult to calculate:

Diagram of eight circular avatar images arranged around a circle. Two line segments identify an A segment in red and a B segment in green. The first segment refers to the current image represented by i. The second segment points to the next image, represented by i plus 1.

The idea is to start from the center of the current image (50% 50%) and move to the center of the next image (X And Y). I first follow segment A to reach the center of the large circle and then follow segment B to reach the center of the next image.

This is the formula:

X = 50% - Ax + Bx
Y = 50% - Ay + By

Ax And Ay are the projections of segment A on the X-axis and Y-axis. We can use trigonometric functions to obtain the values.

Ax = r x sin(i);
Ay = r x cos(i);

The r represents the radius of the circle defined by the CSS variable --_rAnd i represents the rotation angle defined by the CSS variable --_i.

Same logic with the B segment:

Bx = r x sin(j);
By = r x cos(j);

The j is similar to ibut for the next image in the series, which means we increase the index by 1. That gives us the following CSS calculations for each variable:

--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());

And the last code with the mask:

.container {
  --s: 120px; /* image size */
  --g: 14px;  /* the gap */

  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());
  transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
  mask: 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, right? You may see two different implementations for the cutout. The formula I used before took into account the following image, but if we look at the last Instead, the cut goes in a different direction. So instead of increasing the index, we decrease it and assign it to a .reverse class we can use if we want the crop to go in the opposite direction:

.container img {
  --_j: calc(1turn*(sibling-index() + 1)/sibling-count());
}
.container.reverse img {
  --_j: calc(1turn*(sibling-index() - 1)/sibling-count());
}

The animation part

Similar to what we did in the previous article, the purpose of this animation is to remove the overlap when hovering over an image to make it fully visible. In the horizontal list we simply set its value margin ownership 0and we adjust the margin of the other images to avoid overflow.

This time the logic is different. We will rotate all images except the floating image until the floating image is fully visible. The direction of rotation obviously depends on the cutting direction.

Eight avatar images arranged around a circle. An arrow points to the same thing, showing what happens when you hover over the avatar at the top of the circle.

To rotate the image we need to use the --_i variable, which is used as an argument for the rotation function. Let’s start with an arbitrary value for the rotation, for example 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 you hover over an image, all the images will rotate by 20deg. Try it out in the next demo.

Hmm, the images do rotate, but the mask doesn’t follow. Remember that the mask takes into account the position of the next or previous image defined by --_j and the next/previous image rotates — that’s why we also need 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 a lot of redundant code. Let’s optimize it a bit by defining additional 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 defined in one place, and I consider two intermediate variables to add an offset to the --_i And --_j variables.

The rotation of all images is now perfect. Let’s disable the rotation of the floating image:

.container img:hover {
  --_ii: 0deg;
  --_jj: 0deg;
}

Oops, the mask is off again! Do you see the problem?

We want to prevent the image the mouse pointer is hovering over from rotating, while the rest of the images can rotate. That’s why the --_j The floating image variable needs to be updated because it is linked to the next or previous image. So we have to delete --_jj: 0deg and keep alone --_ii: 0deg.

.container img:hover {
  --_ii: 0deg;
}

That’s a little better. We fixed the cropped effect on the floating image, but the overall effect is still not perfect. Let’s not forget that the floating image is the next or previous image of another image, and because it does not rotate, another image can --_j variable must remain unchanged.

For the first list, it is the variable from the previous image that should remain unchanged. For the second list, this is the variable from the following image:

/* select previous element of hovered */
.container:not(.reverse) img:has(+ :hover),
/* select next element of hovered */
.container.reverse img:hover + * {
  --_jj: 0deg;
}

In case you’re wondering how I went about doing this, I tried both ways and I chose the one that worked. It was the code above or this:

.container:not(.reverse) img:hover + *,
.container.reverse img:has(+ :hover) {
  --_jj: 0deg;
}

We’re getting closer! All images behave correctly except one in each list. Try to place them all to identify the culprit.

Can you find out what we’re missing? Think about it for a moment.

Our list is round, but the HTML code is not, so even if the first and last images are visually juxtaposed, the code is not. We can’t link them both using the adjacent sibling selector (+). We need two more selectors to cover these edge cases:

.container.reverse:has(:last-child:hover) img:first-child,
.container:not(.reverse):has(:first-child:hover) img:last-child {
  --_jj: 0deg;
}

Phew! We fixed all the issues and now our hover effect is great, but it’s still not perfect. Instead of using an arbitrary value for the rotation, we need to be precise. We need to find the smallest value that removes the overlap while keeping the images as close together as possible.

Shows the gap between two images at three different points. The first and third points are too close and too far apart, respectively. The centerpiece is perfect with just enough space between the images.

We can figure out the value with some trigonometry. I’ll skip geometry class again (we have enough headaches already!) and give you the value:

--_a: calc(2*asin((var(--s) + var(--g))/(2*var(--_r))) - 1turn/sibling-count());

Now we can say that everything is perfect!

Conclusion

This one was a bit difficult, right? Don’t worry if you’re a little lost with all the complex formulas. They’re very specific to this example, so even if you’ve already forgotten them, that’s fine. The goal was to explore some modern features and a few CSS tricks, such as offset, mask, sibling-* functions, container query units, min()/max()and more!

Responsive list of avatars using modern CSS

  1. Horizontal frames
  2. Circular Lists (You are here!)

#Responsive #list #avatars #modern #CSS #part #CSS #tricks

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *