Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 1)

Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 1)

In a previous article on CSS-Tricks, I demonstrated how to create a fancy hover effect where an avatar pops out of a circle on hover. The challenge was to make such an effect using only the <img> tag.

I know it might seem like this shape requires advanced trickery. But if we break it down a bit, all we’re really talking about is a series of small circles around a much larger circle.

We are going to rely on radial-gradient and some math, specifically trigonometric functions. Bramus Van Damme provides an excellent primer on trigonometric functions over at web.dev. It’s very much worth your while to brush up on the concept with that article.

We are going to define two variables to control the flower shape. N represents the number of the small circles, and R is the diameter of the small circles (Illustrated by the black arrow in the figure above). If we have the diameter, then we can calculate the radius by dividing R by 2. This is everything we need to create the shape!

Here is what the code of a small circle looks like:

img {
  --r: 50px;
  mask:
    radial-gradient(#000 70%, #0000 72%) no-repeat
    {position} / var(--r) var(--r);
}

All of the small circles can use the same radial gradient. The only difference between them is the position. Here comes the math:

(50% + 50% * cos(360deg * i/N)) (50% + 50% * sin(360deg * i/N))

N is the number of circles, and i is the index of each circle. We could manually position each circle individually, but that’s a lot of work, and I believe in leveraging tools to help do some of the heavy lifting. So, I’m going to switch from CSS to Sass to use its ability to write loops and generate all of the circle positions in one fell swoop.

$n: 15; /* number of circles */

img {
  --r: 50px; /* control the small circles radius */
  $m: ();
  @for $i from 1 through ($n) {
    $m: append($m, 
         radial-gradient(#000 70%,#0000 72%) no-repeat
          calc(50% + 50% * cos(360deg * #{$i / $n})) 
          calc(50% + 50% * sin(360deg * #{$i / $n})) /
            var(--r) var(--r), 
        comma);
   }
  mask: $m;
}

We’re essentially looping through the number of circles ($n) to define each one by chaining the radial gradient for each one as comma-separated values on the mask ($m) that is applied to the image element.

We still need the large circle that the small circles are positioned around. So, in addition to the loop’s output via the $m variable, we chain the larger circle’s gradient on the same mask declaration:

img {
  /* etc */
  mask: $m, radial-gradient(#000 calc(72% - var(--r)/2),#0000 0);
}

Finally, we define the size of the image element itself using the same variables. Calculating the image’s width also requires the use of trigonometric functions. Then, rather than doing the same thing for the height, we can make use of the relatively new aspect-ratio property to get a nice 1:1 ratio:

img {
  /* etc */
  width: calc(var(--r) * (1 + 1/tan(180deg / #{$n})));
  aspect-ratio: 1;
}

Check it out. We have the shape we want and can easily control the size and number of circles with only two variables.

We’re basically reducing the distance of the small circles, making them closer to the center. Then, we reduce the size of the larger circle as well. This produces an effect that appears to change the roundness of the smaller circles on hover.

The final trick is to scale the entire image element to make sure the size of the hovered shape is the same as the non-hovered shape. Scaling the image means that the avatar will get bigger and will pop out from the frame that we made smaller.

$n: 15; /* number of circles */

@property --i {
  syntax: "<length>";
  initial-value: 0px;
  inherits: true;
}

img {
  /* CSS variables */
  --r: 50px; /* controls the small circle radius and initial size */
  --f: 1.7; /* controls the scale factor */
  --c: #E4844A; /* controls the main color */

  $m: ();
  /* Sass loop */
  @for $i from 1 through ($n) {
    $m: append($m, 
      radial-gradient(var(--c) 70%, #0000 72%) no-repeat
      calc(50% + (50% - var(--i, 0px)) * cos(360deg * #{$i/$n} + var(--a, 0deg))) 
      calc(50% + (50% - var(--i, 0px)) * sin(360deg * #{$i/$n} + var(--a, 0deg))) /
      var(--r) var(--r), 
    comma);
  }

  mask: 
    linear-gradient(#000 0 0) top/100% 50% no-repeat,
    radial-gradient(var(--c) calc(72% - var(--r)/2 - var(--i, 0px)), #0000 0),
    $m;
  background:
    radial-gradient(var(--c) calc(72% - var(--r)/2 - var(--i, 0px)), #0000 0),
    $m;
  transition: --i .4s, scale .4s;
}

img:hover {
  --i: calc(var(--r)/var(--f));
  scale: calc((1 + 1/tan(180deg/#{$n}))/(1 - 2/var(--f) + 1/tan(180deg/#{$n})));
}

Here’s what’s changed:

  • The Sass loop that defines the position of the circle uses an equation of 50% - var(--i, 0px) instead of a value of 50%.
  • The larger circle uses the same variable, --i, to set the color stop of the main color in the gradients that are applied to the mask and background properties.
  • The --i variable is updated from 0px to a positive value. This way, the small circles move position while the large circle becomes smaller in size.
  • The --i variable is registered as a custom @property that allows us to interpolate its values on hover.

You may have noticed that I didn’t mention anything about the --f variable that’s defined on the image element. Truthfully, there is no special logic to it. I could have defined any positive value for the variable --i on hover, but I wanted a value that depends on --r, so I came up with a formula (var(--r) / var(--f)), where --f allows controls the scale.

Does the equation on the scale property on hover give you a little bit of panic? It sure looks complex, but I promise you it’s not. We divide the size of the initial shape (which is also the size of the element) by the size of the new shape to get the scale factor.

  • The initial size: calc(var(--r)*(1 + 1 / tan(180deg / #{$n})))
  • The size of the new shape: calc(var(--r) * (1 + 1 / tan(180deg / #{$n})) - 2 * var(--r) / var(--f))

I am skipping a lot of math details to not make the article lengthy, but feel free to comment on the article if you want more detail on the formulas I am using.

That’s all! We have a nice “pop out” effect on hover:

See the Pen Fancy Pop Out hover effect! by Temani Afif.

Wrapping Up

Does all of this seem a bit much? I see that and know this is a lot to throw at anyone in a single article. We’re working with some pretty new CSS features, so there’s definitely a learning curve with new syntaxes, not to mention some brushing up on math functions you probably haven’t seen in years.

But we learned a lot of stuff! We used gradients with some math to create a fancy shape that we applied as a mask and background. We introduced @property to animate CSS variables and bring our shape to life. We also learned a nice trick using animation-composition to control the speed of the rotation.

We still have a second part of this article where we will reuse the same CSS techniques to create a fancier hover effect, so stay tuned!

I’ll leave you with one last demo as a sign-off.

See the Pen Pop out hover effect featuring Lea and Una by Temani Afif.