Text reveal with clip path

There is this effect where you can use clip path to reveal a text (or any other element) by tracking the mouse position and revealing some other form. I wanted to explore how this effect is done and how we could implement it.

How it’s done

The main idea behind this idea is to have two layers. One layer performs the initial display while the bottom layer is displaying the second portion

Bruno

Bruno

Now that we have these two layers positioned on top of each other, we can use clip-path to create our “mask”.

How clip-path works

In [Part 1] Building mountains: CSS’s clip-path I go a bit deeper on how clip-path actually works but for this effect we only need to know that clip-path hides certain parts of an element.

clip-path: inset() hides elements by cutting away edges from all four sides. You give it values for top, right, bottom, and left, only the area inside these boundaries shows. For example, inset(50% 0% 0% 0%) hides 50% from top:

clip-path: inset(0% 0% 0% 0%)

Now that we know we can hide a certain element using clip-path we can apply the same logic by having two layers, and when hovering over the element, we can hide half of one element while displaying the other

The math behind tracking the line

The tricky part isn’t the clip-path. It’s converting a mouse position into the right percentage. Two coordinate systems are at work here, and the reveal only lines up if you switch between them correctly.

Mouse position

The mouse move handler gets the cursor’s Y position relative to the container. You turn that into a percentage of the container’s height, then clamp it so the line stops at 10% and 90% (otherwise it looks like the text just disappears)

const mouseY = event.clientY - containerRect.top;
const percentage = Math.max(10, Math.min((mouseY / containerRect.height) * 100, 90));

This percentage positions the tracking line with top: percentage%.

Line position

Here’s the catch: clip-path’s percentages are relative to the element being clipped, not the container. Our text (z1/z2) is centered and shorter than the container, so “50% of the container” and “50% of the text” point at different heights. If we clipped using the container’s relative percentage directly, the line and the reveal edge would drift apart as soon as the text’s box did not match the container’s.

To fix that, we re-measure where the line actually landed (in viewport pixels, via getBoundingClientRect) and re-express it as a percentage of the text’s height

const relativeLinePosition = ((lineRect.top - z1Rect.top) / z1Rect.height) * 100;

This subtracts the text’s top position from the line’s top position (that’s how far down the line sits), then divides by the text’s height to get a percentage the text understands.

Now we use that percentage in both clip-paths

z1.style.clipPath = `polygon(0 0, 100% 0, 100% ${relativeLinePosition}%, 0 ${relativeLinePosition}%)`;
z2.style.clipPath = `polygon(0 ${relativeLinePosition}%, 100% ${relativeLinePosition}%, 100% 100%, 0 100%)`;

z1 keeps everything above the line. z2 keeps everything below it. The line always lands exactly where you see it, no matter how the text is sized or positioned inside the container.

<div class="wrapper">
  <div class="container" data-container>
      <div class="tracking-line" data-tracking-line></div>

      <h2 class="text z1" data-z1>Bruno</h2>
      <h2 class="text z2" data-z2>Bruno</h2>
  </div>
</div>

Bruno

Bruno

One more usage of clip-path is to switch between images and create this simple effect

Which cars would you like to drive today?

Ford mustange carFerrari Monza car

Navigation