Masking as a Storytelling Tool
Keyframe and scroll-driven animations are powerful, but they often feel flat. The web is ready for effects that suggest depth, layers and a more cinematic quality. As I explored in Part 1, the limited animation style of 1960s cartoons offers a rich source of inspiration for modern web design. While working with Mike Worth on his site, I found that CSS masking adds that missing dimension by controlling not just when things appear, but how they are revealed.
To understand masking, consider the classic cartoon technique of the iris-out, where a vignette closes in on a character's face. The content is never erased, only hidden. In CSS, masks work on the same principle: they use a bitmap, vector, or gradient image to control an element's visibility. Where the mask has filled pixels, the element shows; where it's transparent, the content hides.
Clipping Paths for Sharp Edges
Need a reveal with a hard, defined edge, like a character entering a cave? A clip-path is often the right tool. Although technically different from masks, they serve a similar purpose. In this case, the illustration of the cave has a hard-edged opening, making a clipping path the ideal choice.
On Mike's biography page, his orangutan character slides into a cave. The SVG has two groups: one for the background and one for the foreground mascot. Animating the mascot's translate value moves him across the frame from 2000px to his natural position. While a 50px bounce adds realism, the character was still visible at the viewport's edge before entering the cave's mouth.
CSS offers several ways to define a clip-path, from basic shapes to SVG paths:
- Primitive rectangle: Use four values for corners, with
roundfor rounded corners. xywhvalues: Specify x, y, width, and height.- Circles and ellipses: Define with
circle()orellipse(). - Polygons: Use
polygon()with a set of points. - SVG path: Reference points from a `path` created in a graphics app like Sketch.
clip-path: rect(0px 150px 150px 0px round 5px);
clip-path: polygon(...);
clip-path: path("M ...");
For this specific example, I opted for a clipping path defined by an SVG. To use this, create a separate SVG file and hide it by setting its dimensions to zero. Place the clipPath inside the defs element so it isn't rendered but is available for CSS to reference.
<figure>
<svg viewBox="0 0 1400 960" id="cave">...</svg>
<svg height="0" width="0" id="mask">...</svg>
</figure>
<svg height="0" width="0" id="mask">
<defs>
<clipPath id="mask-cave">...</clipPath>
</defs>
</svg>
With the clipPath URL applied, the mascot becomes visible only at the cave's edge, fixing the odd reveal.
Tip: To make theclipPathresponsive, addclipPathUnits="objectBoundingBox"to its opening tag. Then, scale it by dividing1by the SVG's width and height. For an SVG of width1400px, this gives a scale value of0.0007142857143.
<clipPath id="mask-cave"
clipPathUnits="objectBoundingBox"
transform="scale(0.0007142857143, 0.001041666667)">
...
</clipPath>
Choosing a Mask Image
When you need to alter an illustration's content—by overlaying colors or changing its shape—clip-path can be heavy and performance-intensive. In that case, a CSS mask is a more efficient option, since it's been baseline since 2023.
The mask property is a shorthand for several sub-properties including mask-clip, mask-origin, mask-position, mask-repeat, and mask-size. It follows the same syntax and defaults as CSS backgrounds. A mask repeats by default and is placed at the top-left corner, for example, unless you alter these values.
mask-image: url("mask.webp");
/* Options: repeat, repeat-x, repeat-y, round, space, no-repeat */
mask-repeat: no-repeat;
/* Options: Keywords (auto, contain, cover), units, percentages */
mask-size: cover;
For Mike's FAQs page, the goal was to separate the shape of a hero at a crossroads from its content. I created a scalable mask-image defining the visible area and applied it to the containing figure element. By setting the mask-size to cover, it always matches the illustration's dimensions.
figure {
mask-image: url(mask.svg);
}
figure {
mask-size: cover;
}
From Hard Cuts to Soft Focus
Creating a soft focus, like a spotlight on a treasure map, requires more finesse than the hard edge of a clipping path. While combining Gaussian blur with an SVG mask works, it's over-engineered for a simple visual effect. The most elegant solution uses a single radial-gradient to define the mask. This not only produces the soft edges but also requires no extra files and just one CSS property.
figure {
mask-image: radial-gradient(ellipse farthest-corner at center center, #000 0%, transparent 75%);
}
Layering and Animating for Depth
Masks, like background images, can be layered. On the review page where the orangutan studies his map, lighting is key. I combined two masks: a semi-transparent radial-gradient for general ambiance and a 45-degree linear-gradient for light rays. While effective, the light rays were positioned globally. To make them appear to come from the desk lamp, I swapped the linear-gradient for a soft-edged bitmap image with more precise control.
figure {
mask-image:
radial-gradient(circle, rgba(255,16,76,.5) 45%, transparent 55%),
linear-gradient(45deg, transparent 40%, #ff104c 50%, #ff104c 50%, transparent 60%);
mask-repeat: no-repeat;
}
figure {
mask-image:
radial-gradient(circle, rgba(255,16,76,.5) 45%, transparent 55%),
url(mask.webp);
mask-size: 90%, cover;
}
Animating CSS masks can create compelling transitions between scenes or bring focus to content. In a deleted scene, the mascot drives across a landscape while being watched. I added a binocular-shaped mask, applied it to the figure element, and centered it. To bring the animation to life, I added a keyframe animation that shifts the mask-position, creating the feeling that the binoculars are moving with the character.
@keyframes pan-mask {
0% { mask-position: 45% 45%; } /* Start lower-left */
25% { mask-position: 55% 55%; } /* Move to upper-right */
50% { mask-position: 43% 52%; } /* Shift more dramatically */
75% { mask-position: 57% 48%; } /* More variation */
100% { mask-position: 45% 45%; } /* Loop back */
}
To deepen the connection, I made the mask-position follow the mouse cursor. I made the environment feel more interactive by blurring the visible content and removing the filter only when the user presses the spacebar or clicks a mouse button.
<script>
// Select the figure element.
const figure = document.querySelector('figure');
document.addEventListener('mousemove', (event) => {
// Get the cursor position.
const mouseX = event.clientX;
const mouseY = event.clientY;
// Normalise the mask-position.
const maskX = (mouseX / window.innerWidth) * 100;
const maskY = (mouseY / window.innerHeight) * 100;`
// Dynamically set the mask-position.
figure.style.maskPosition =${maskX}% ${maskY}%;
});
</script>
<script>
// When mouse button pressed, remove blur
document.addEventListener('mousedown', () => {
figure.style.filter = 'blur(0)';
});
// When mouse button released, reapply blur
document.addEventListener('mouseup', () => {
figure.style.filter = 'blur(5px)';
});
// When spacebar pressed, remove blur
document.addEventListener('keydown', (event) => {
if (event.key === ' ') {
figure.style.filter = 'blur(0)';
}
});
// When spacebar released, reapply blur
document.addEventListener('keyup', (event) => {
if (event.key === ' ') {
figure.style.filter = 'blur(5px)';
}
});
</script>
Masking as a Storytelling Tool
Mike Worth’s website puts a playful spin on navigation errors: when someone “takes a wrong turn,” they are treated to an animation of a character sinking into hot lava.
A zoom-in effect, similar to the opening example of this article, works well to signal that the user has reached a dead end and recapture their attention. The technique uses a circular clip-path that defaults to 75% of the element’s size. Keyframes then shrink that circle down to 15%, with the animation attached to the figure using a one-second duration and a three-second delay:
@keyframes error {
0% { clip-path: circle(75%); }
100% { clip-path: circle(15%); }
}
figure {
clip-path: circle(75%);
animation: error 1s 3s ease-in forwards;
}
This pull toward the character makes the moment feel focused and intentional — drawing the eye down as the figure starts to sink into the lava below.
See the Pen [Mike Worth’s error page [forked]](https://codepen.io/smashingmag/pen/qEEgdxy) by Andy Clarke.
The full effect works as a self-contained demo you can try directly:
<figure>
<svg>…</svg>
</figure>
Why Masking Matters
Masking expands what’s possible in web animation. It lets you reveal content, direct the viewer’s gaze, and add depth without weighing down the page. The effects remain lightweight because they rely on CSS-native properties rather than heavy JavaScript or image assets.
The creative range is broad — from subtle transitions to dramatic reveals — and experimenting with different mask shapes, timing, and easing functions can unlock effects that feel distinctly three-dimensional.
Worth’s site is scheduled to launch in June 2025, with a preview of the animations available now in a CodePen collection.




