Creating a clipping mask hover effect with Tailwind CSS

You might have noticed how on this site I use certain hover effects where an element previously invisible presents itself and its contents are in a different color. It sort of swipes in. You can see it on blocks linking to a certain project or journal entry or when invoking the mobile navigation. In this post I'm going to show you how you can achieve this with Tailwind CSS.

The effect itself is a little hard to put in words, but check out this next example and I'm sure you know what I mean. I've slowed this animation down a bit to make it a little more clear what's actually happening.

Ik ga ermee akkoord dat mijn data wordt opgeslagen en verwerkt. Meer informatie
An example of the hover effect we're going to build.

You can also check out the effect with some more details on the homepage for example. Scroll down to projects or journal and see what happens when you hover those blocks.

How does this work

This effect works by using two stacked layers. The inverted layer with the tinted background is on top but completely clipped using a CSS clip-path. As soon as we hover we transition that clipping path so that it's completely visible. The CSS involved looks like this.

1.default-state {
2 clip-path: circle(20% at 120% 120%);
3}
4 
5.hovered-state {
6 clip-path: circle(170% at 120% 120%);
7}
The two states we're going to transition.

In the default state we have a circle with a diameter of 20% positioned on 120% on the x-axis and 120% on the y-axis. This means we can't see anything of the clipped content. When we hover however the circle increases in size to 170% and that'll make the clipped content visible. A great way to learn about this syntax is by playing with Clippy by Bennet Feely.

Tailwind CSS and arbitrary properties

Now let's convert these properties to Tailwind CSS using arbitrary properties for the clipping paths.

1<div
2 class="
3 [clip-path:circle(20%_at_120%_120%)]
4 group-hover:[clip-path:circle(170%_at_120%_120%)]
5 "
6>
The CSS properties written in Tailwind CSS using arbitrary properties.

Having this magic up our sleeves, all we need to do now is combine it with the actual markup. Let's start with the basic shape of the button.

1<a href="#" class="py-6 px-12 border-2 border-indigo-600 group text-indigo-600">
2 <span class="text-4xl uppercase font-light tracking-wider">Hover me</span>
3</a>
The button basics.

If we position this button relatively we can use an absolutely positioned div inside it with the color inverted button markup.

1<a href="#" class="relative py-6 px-12 border-2 border-indigo-600 group text-indigo-600">
2 <span class="text-4xl uppercase font-light tracking-wider">Hover me</span>
3 <div class="absolute inset-0 py-6 px-12 bg-indigo-600 text-white">
4 <span class="text-4xl uppercase font-light tracking-wider">Hover me</span>
5 </div>
6</a>
The button duplicated and color inverted inside itself.

By adding a transition utility and the arbitrary clipping mask properties we can make sure the color inverted button duplicate is masked by default and will reveal itself when the parent (group) is hovered. I use the motion-safe modifier on all my transitions to make sure the effect is only visible when users don't explicitly toggled on reduce motion in their operating system settings.

1<a href="#" class="relative py-6 px-12 border-2 border-indigo-600 group text-indigo-600">
2 <span class="text-4xl uppercase font-light tracking-wider">Hover me</span>
3 <div class="absolute inset-0 py-6 px-12 bg-indigo-600 text-white motion-safe:transition-[clip-path] motion-safe:duration-500 ease-out [clip-path:circle(20%_at_120%_120%)] group-hover:[clip-path:circle(170%_at_120%_120%)]">
4 <span class="text-4xl uppercase font-light tracking-wider">Hover me</span>
5 </div>
6</a>
The button with added transitions and clip-paths.

Let's finish it off by adding some attributes to improve accessibility.

1<a href="#" draggable="false" aria-labelledby="title" class="relative py-6 px-12 border-2 border-indigo-600 group text-indigo-600">
2 <span id="title" class="text-4xl uppercase font-light tracking-wider">Hover me</span>
3 <div class="absolute inset-0 py-6 px-12 bg-indigo-600 text-white motion-safe:transition-[clip-path] motion-safe:duration-500 ease-out [clip-path:circle(20%_at_120%_120%)] group-hover:[clip-path:circle(170%_at_120%_120%)]" aria-hidden="true">
4 <span class="text-4xl uppercase font-light tracking-wider">Hover me</span>
5 </div>
6</a>
The final result.

As you can see we've added draggable, aria-labeledby and aria-hidden attributes. The first one instructs the browser that this thing is not draggable. The second one makes sure that screen readers have a title to pronounce and the third one instructs screen readers ignore this duplicate content.

Did you enjoy this article or do you have any questions? Let me know on Twitter.

Making a clipping mask with an inline SVG

  • Tailwind CSS
  • CSS