Animating clip-path and Mask Reveals
Part of Compositor-Only Property Optimization in Performance Budgeting & GPU Architecture.
The problem
Wipes and shape reveals are a staple of editorial and marketing motion: an image uncovered left to right, a hero expanding from a circle around the cursor, a section heading revealed behind a sliding edge. clip-path makes them easy to write — animate inset(0 100% 0 0) to inset(0 0 0 0) and the wipe is done. On a phone, a full-width wipe over a large image drops frames, and the same effect with a gradient mask-image is worse.
The question is whether these properties are compositor-friendly, and if not, how to get the same look without paying for paint on every frame.
Root cause analysis: a clip changes what is painted
transform and opacity change how an already-painted layer is drawn. clip-path and mask change which pixels of the element are visible, and in most engines that is resolved during paint: each frame computes the new clip region, invalidates the area it affects and repaints the element’s content inside it. The cost scales with the area of the element, not with how much of the clip moved.
Engines have been working on running some clip-path animations on the compositor — the clip is geometry the GPU can apply to an existing texture — but support is partial, depends on the shape type and on the element not being affected by other paint-dependent properties, and is not something to build on without verifying it in your target browsers. The safe assumption is that a clip-path animation is a paint animation, as the compositor-only property reference classifies it.
Masks are more expensive still. A gradient mask-image must be rasterised and combined with the element’s pixels, and animating mask-position or mask-size repeats that per frame. url() references to SVG <clipPath> or <mask> elements add a layer of indirection and are consistently main-thread work.
Decision matrix
| Reveal | Element size | Use |
|---|---|---|
| Rectangular wipe | Large (hero, full-width image) | Transform wrapper with counter-transformed child |
| Rectangular wipe | Small (button, thumbnail, heading) | clip-path: inset() |
| Circle or ellipse from a point | Any, brief (under 400ms) | clip-path: circle() with containment |
| Circle from a point | Large, slow | A scaled circular element over the content, or a cross-fade |
| Soft-edged gradient wipe | Small | mask-image with animated mask-position |
| Soft-edged gradient wipe | Large | A gradient overlay element animated with transform |
| Arbitrary shape morph | Any | clip-path: polygon() with the same number of points in both states |
Polygon morphs follow the same rule as SVG path morphing: both polygon() values need the same number of points, or the property swaps discretely at the midpoint instead of interpolating.
Production code pattern
<figure class="wipe">
<div class="wipe__window">
<img class="wipe__image" src="harbour.avif" alt="Harbour at dawn" width="1200" height="800">
</div>
</figure>
/* Large wipe on the compositor: move a clipping window right, move its content left. */
.wipe__window {
overflow: clip; /* the window clips; no clip-path involved */
transform: translateX(-100%); /* window starts fully to the left */
}
.wipe__image {
display: block;
transform: translateX(100%); /* content offset equally the other way: stays put */
}
.wipe.is-revealed :is(.wipe__window, .wipe__image) {
transform: translateX(0);
transition: transform 700ms cubic-bezier(0.65, 0, 0.35, 1);
}
/* Small element: clip-path is simpler and the paint area is trivial. */
.tag {
clip-path: inset(0 100% 0 0);
contain: paint; /* bound the repaint to the tag's box */
transition: clip-path 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tag.is-revealed {
clip-path: inset(0 0 0 0);
}
@media (prefers-reduced-motion: reduce) {
.wipe__window, .wipe__image { transform: none; }
.wipe.is-revealed :is(.wipe__window, .wipe__image) { transition: none; }
.tag { clip-path: none; opacity: 0; transition: opacity 150ms linear; }
.tag.is-revealed { opacity: 1; }
}
Rendering Impact: composite for the wipe, small paint for the tag. The window and image are two layers moving in opposite directions by the same distance, so the image appears stationary while the visible region grows; nothing is repainted after the first frame.
The wrapper technique relies on two transforms cancelling exactly. Both elements use the same timing function and duration, and because both are applied on the compositor they stay in lock-step. It works for any rectangular wipe direction — swap the axis and sign — but not for circular or diagonal shapes, which is where clip-path with containment remains the pragmatic choice.
Verification checklist
Constraints and trade-offs
- The transform wrapper needs an extra element and only produces rectangular wipes.
- Two promoted layers for the wipe cost texture memory for the duration; release any
will-changeonce the reveal completes. clip-pathanimations may be composited in some engines in some cases; do not rely on it, but do not be surprised when a trace shows less paint than expected.overflow: clipdoes not create a scroll container, which keeps the wrapper cheap, but it also clips focus rings on focusable content inside.- Soft-edged reveals with masks are the most expensive option and should be brief even on small elements.
Frequently asked questions
Is clip-path animation hardware accelerated?
Not reliably. In most engines and most cases it is resolved during paint on the main thread. Some engines can composite certain clip-path animations, but support varies, so treat it as a paint animation and verify in DevTools.
Why does my polygon clip-path jump instead of animating?
The start and end polygons have different numbers of points. Interpolation pairs points by index, so both values must list the same count; add duplicate points to the simpler shape.
Can I animate mask-image itself?
Images are not interpolable, so the mask swaps discretely. Animate mask-position or mask-size of a gradient mask instead, and keep the element small, because every frame recomposites the mask.
Is the transform wrapper accessible?
Yes. The content is present and in reading order throughout; only its visible region changes. Make sure the reveal is not the only indication that content has appeared, and that clipped focus indicators are handled.
Related
- Compositor-Only Property Optimization — the property tiers this guide works within
- Replacing box-shadow Animation with Layered Opacity — the same substitution idea for shadows
- Using contain to Scope Repaints — bounding the clip-path repaint