clip-path Techniques
10 minclip-path Techniques
clip-path is the most underrated animation primitive in CSS. While most developers reach for opacity and transform for every reveal, clip-path can produce effects that are impossible with either—directional reveals, wipe transitions, hold-to-delete confirmations, and comparison sliders—all with a single animatable property.
The key insight: clip-path: inset() defines a rectangular clipping region with four values (top, right, bottom, left), and each value can be animated independently. This makes it a four-axis reveal tool—you can reveal an element from any edge, any corner, or any combination.
inset() as the universal primitive
/* Fully visible */
clip-path: inset(0 0 0 0);
/* Hidden from the right (clipped to nothing from the right edge) */
clip-path: inset(0 100% 0 0);
/* Hidden from the bottom */
clip-path: inset(0 0 100% 0);
/* Revealed from the left edge */
clip-path: inset(0 0 0 0); /* animate from: inset(0 100% 0 0) */
Unlike scaleX or width animations, inset() clips without reflowing the layout. The element maintains its full size and position in the document—only its visible region changes. This means no layout thrashing, no width recalculation, and GPU-compositable performance when combined with will-change: clip-path.
clip-path: inset(0% 100% 0% 0%) → inset(0% 0% 0% 0%)Hold-to-delete
The hold-to-delete pattern uses clip-path to create a progress-based reveal. As the user holds down a button, a red fill "wipes" across the button from left to right. If they release before completion, it reverses. If they hold long enough, the delete action fires.
.delete-button {
position: relative;
}
.delete-button::before {
content: "";
position: absolute;
inset: 0;
background: var(--destructive);
clip-path: inset(0 100% 0 0);
transition: clip-path 1.5s linear;
}
.delete-button:active::before {
clip-path: inset(0 0 0 0);
}
The linear timing is intentional—progress indicators should use linear easing so the user can predict when the action will complete. The 1.5s duration gives enough time for the user to read the confirmation and release if they change their mind.
In React, track the hold time with onPointerDown / onPointerUp and fire the action only if the full duration elapsed. The clip-path animation runs on the CSS side, so there's no JavaScript on every frame—just a start event and an end event.
Tab transitions
clip-path produces clean directional transitions between tab panels. Instead of fading between panels (which loses spatial context) or sliding them (which requires overflow management), clip the new panel in from the direction the user is navigating.
Moving from Tab 1 to Tab 3: the new panel reveals from right to left—inset(0 0 0 0) from inset(0 100% 0 0). Moving from Tab 3 to Tab 1: the new panel reveals from left to right—inset(0 0 0 0) from inset(0 0 0 100%).
const direction = newIndex > oldIndex ? "right" : "left";
const initial = direction === "right"
? "inset(0 100% 0 0)"
: "inset(0 0 0 100%)";
This creates the illusion of sliding without actually translating the element. The new content appears in place, revealed directionally—which is often smoother than a transform-based slide because there's no need to position two panels side by side.
Image reveals
clip-path image reveals are a staple of editorial and portfolio design. An image starts fully clipped and reveals as the user scrolls into view, creating a cinematic "curtain" effect.
The simplest version reveals from bottom to top:
.image-reveal {
clip-path: inset(100% 0 0 0);
transition: clip-path 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.image-reveal.visible {
clip-path: inset(0 0 0 0);
}
For more dramatic reveals, use clip-path: polygon() to create diagonal or angular wipes. But for most product UI, inset() is sufficient and far easier to maintain.
Trigger the .visible class with an Intersection Observer. Set the threshold to 0.1 or 0.2—the reveal should start when the image is partially in view, not when it's fully visible. This prevents the jarring "jump" of an image suddenly appearing at full size.
Comparison sliders
The before-and-after comparison slider is a natural fit for clip-path. The "before" image sits on top, clipped to the left portion of the container. The "after" image sits behind at full width. The user drags a handle to adjust the clip boundary.
.comparison-before {
position: absolute;
inset: 0;
clip-path: inset(0 var(--clip-right) 0 0);
}
As the user drags, update --clip-right as a percentage: 100% - dragPosition%. This approach is simpler and more performant than the common technique of adjusting the "before" element's width, because clip-path doesn't trigger layout recalculation.
Combine with setPointerCapture for smooth drag tracking (see the gesture-and-drag lesson) and a touch-action: none on the slider handle to prevent scroll interference.
Rounded insets
inset() accepts an optional round parameter that applies border-radius to the clip shape:
clip-path: inset(8px round 12px);
This is useful for creating padded, rounded reveals—for instance, a notification card that reveals from its centre with rounded corners, creating a "growing window" effect that matches the card's own border-radius.
Performance notes
clip-path animations are GPU-compositable in modern browsers when the clip shape is a simple inset() or circle(). polygon() clips may cause paint on every frame—test with the browser's paint-flashing tool before shipping polygon-based animations in performance-critical paths.
Add will-change: clip-path before the animation starts and remove it after completion. As with all will-change usage, leaving it permanently promotes the element to its own compositor layer and wastes GPU memory.
When not to use clip-path
clip-path creates hard edges. If you need a soft, feathered reveal—a gradient fade from visible to transparent—use a mask-image with a CSS gradient instead. clip-path is binary: pixels are either visible or clipped. For most UI transitions, this is exactly what you want. For atmospheric, editorial effects, consider masks.
Also avoid clip-path for elements with interactive children near the clip boundary. Clipped elements are still in the DOM and still receive events—a button that's visually clipped but still clickable creates a confusing invisible-hit-target problem. Ensure that any clipped region is also pointer-events: none if its children should not be interactive.