In a previous article, we explored fancy hover effects for images that involve shines, rotations, and 3D perspectives. We are going to continue playing with images, but this time, we are making animations that reveal images on hover. Specifically, we will learn about CSS masks and how they can be used to cover portions of an image that are revealed when the cursor hovers over the image.
Here is the HTML we will use for our work:
<img src="" alt="">
Yes, that’s right, only one image tag. The challenge I like to take on with each new CSS project is: Let CSS do all of the work without extra markup.
As we go, you may notice minor differences between the code I share in the article and what is used inside the demos. The code throughout the article reflects the CSS specification. But since browser support is inconsistent with some of the features we’re using, I include prefixed properties for broader support.
Example 1: Circle Reveal
In this first one, an image sits in a square container that is wiped away on hover to reveal the image.
So, we have two images, each with a gradient background that is revealed with a touch of padding. We could have added a
— or perhaps even a — to the markup to create a true container, but that goes against the challenge of letting CSS do all of the work.
While we were able to work around the need for extra markup, we now have to ask ourselves: How do we hide the image without affecting the gradient background? What we need is to hide the image but continue to show the padded area around it. Enter CSS masks.
It’s not terribly complicated to apply a mask to an element, but it’s a little trickier in this context. The “trick” is to chain two mask layers together and be more explicit about where the masks are applied:
content-box: one that is restricted to the image’s content,
padding-box: one that covers the whole image area, including the padded area.
We need two layers because then we can combine them with the CSS mask-composite property. We have different ways to combine mask layers with mask-composite, one of which is to “exclude” the area where the two masks overlap each other.
Notice that we can remove the padding-box from the code since, by default, a gradient covers the whole area, and this is what we need.
Are there other ways we could do this without mask-composite? There are many ways to hide the content box while showing only the padded area. Here is one approach using a conic-gradient as the mask:
mask:
conic-gradient(from 90deg at 10px 10px, #0000 25%, #000 0)
0 0 / calc(100% - 10px) calc(100% - 10px);
/* 10px is the value of padding */
There are others, of course, but I think you get the idea. The approach you choose is totally up to you. I personally think that using mask-composite is best since it doesn’t require us to know the padding value in advance or change it in more than one place. Plus, it’s a good chance to practice using mask-composite.
Now, let’s replace the second gradient (the one covering only the content area) with a radial-gradient. We want a circle swipe for the hover transition, after all.
See that? The exclude mask composite creates a hole in the image. Let’s play with the size and position of that cutout and see what is happening. Specifically, I’m going to cut the size in half and position the circle in the center of the image:
I bet you can already see where this is going. We adjust the size of the radial-gradient to either hide the image (increase) or reveal the image (decrease). To fully hide the image, we need to scale the mask up to such an extent that the circle covers up the image. That means we need something greater than 100%. I did some boring math and found that 141% is the precise amount, but you could wing it with a round number if you’d like.
We start with a size equal to 150% 150% to initially hide the image. I am taking the additional step of applying the size as a CSS variable (--_s) with the full size (150% 150%) as a fallback value. This way, all we need to do is update the variable on hover.
Add a hover state that decreases the size to zero so that the image is fully revealed.
Apply a slight transition of .5s to smooth out the hover effect.
We just created a nice reveal animation with only a few lines of CSS — and no additional markup! We didn’t even need to resort to pseudo-elements. And this is merely one way to configure things. For example, we could play with the mask’s position to create a slick variation of the effect:
I’m a big fan of putting an idea out there and pushing it forward with more experiments. Fork the demo and let me know what interesting things you can make out of it!
Example 2: Diagonal Reveal
Let’s increase the difficulty and try to create a hover effect that needs three gradients instead of two.
Don’t look at the code just yet. Let’s try to create it step-by-step, starting with a simpler effect that follows the same pattern we created in the first example. The difference is that we’re swapping the radial-gradient with a linear-gradient:
You’ll notice that the other minor difference between this CSS and the first example is that the size of the mask is equal to 200% 200%. Also, this time, the mask’s position is updated on hover instead of its size, going from 0% 0% (top-left) to 100% 100% (bottom-right) to create a swiping effect.
We can change the swipe direction merely by reversing the linear gradient angle from 135deg to -45deg, then updating the position to 0% 0% on hover instead of 100% 100%:
One more thing: I defined only onemask-position value on hover, but we have two gradients. If you’re wondering how that works, the mask’s position applies to the first gradient, but since a gradient occupies the full area it is applied to, it cannot be moved with percentage values. That means we can safely define only one value for both gradients, and it will affect only the second gradient. I explain this idea much more thoroughly in this Stack Overflow answer. The answer discusses background-position, but the same logic applies to mask-position.
Next, I’d like to try to combine the last two effects we created. Check the demo below to understand how I want the combination to work:
This time, both gradients start at the center (50% 50%). The first gradient hides the top-left part of the image, while the second gradient hides the bottom-right part of it. On hover, both gradients slide in the opposite direction to reveal the full image.
If you’re like me, you’re probably thinking: Add all the gradients together, and we’re done. Yes, that is the most intuitive solution, and it would look like this:
This approach kind of works, but we have a small visual glitch. Notice how a strange diagonal line is visible due to the nature of gradients and issues with anti-aliasing. We can try to fix this by increasing the percentage slightly to 50.5% instead of 50%:
Yikes, that makes it even worse. You are probably wondering if I should decrease the percentage instead of increasing it. Try it, and the same thing happens.
The fix is to update the mask-composite property. If you recall, we are already using the exclude value. Instead of declaring exclude alone, we need to also apply the add value to make sure the bottom layers (the swiping gradients) aren’t excluded from each other but are instead added together:
Now, the second and third layers will use the add composition to create an intermediate layer that will be excluded from the first one. In other words, we must exclude all the layers from the first one.
I know mask-composite is a convoluted concept. I highly recommend you read Ana Tudor’s crash course on mask composition for a deeper and more thorough explanation of how the mask-composite property works with multiple layers.
One more small detail you may have spotted: we have defined three gradients in the code but only twomask-position values on the hover state:
img:hover {
mask-position: 0% 0%, 100% 100%;
}
The first value (0% 0%) is applied to the first gradient layer; it won’t move as it did before. The second value (100% 100%) is applied to the second gradient layer. Meanwhile, the third gradient layer uses the first value! When fewer values are declared on mask-position than the number of mask layers, the series of comma-separated values repeats until all of the mask layers are accounted for.
In this case, the series repeats circles back to the first value (0% 0%) to ensure the third mask layer takes a value. So, really, the code above is a more succinct equivalent to writing this:
While this may look like a more complex hover effect than the last two we covered, it still uses the same underlying CSS pattern we’ve used all along. In fact, I’m not even going to dive into the code as I want you to reverse-engineer it using what you now know about using CSS gradients as masks and combining mask layers with mask-composite.
I hope you enjoyed this little experiment with CSS masks and gradients! Gradients can be confusing, but mixing them with masks is nothing short of complicated. But after spending the time it takes to look at three examples in pieces, we can clearly see how gradients can be used as masks as well as how we can combine them to “draw” visible areas.
Once we have an idea of how that works, it’s amazing that all we need to do to get the effect is update either the mask’s position or size on the element’s hover state. And the fact that we can accomplish all of this with a single HTML element shows just how powerful CSS is.
We saw how the same general CSS pattern can be tweaked to generate countless variations of the same effect. I thought I’d end this article with a few more examples for you to play with.