CSS clip-path Animations: Shapes, Reveals and Hover Effects
Master CSS clip-path animations to build polygon reveals, circle wipes, and hover effects that feel native — no JavaScript required for most of it.
What clip-path Actually Does (and Why It Animates So Well)
CSS clip-path cuts an element down to a specific shape. Everything outside that shape is invisible — not hidden with overflow, not moved off-screen, just clipped at the paint step. That distinction matters because the browser can animate it on the compositor thread, which means you get smooth 60fps transitions without triggering layout or paint when you animate between two compatible shapes.
The key word is *compatible*. You can animate from polygon() to polygon() as long as both have the same number of points. You can animate circle() to circle(). Mix them and you'll get a hard snap instead of a smooth transition — which is a common gotcha that trips people up in 2026 even though the spec has been stable since Chrome 55.
Honestly, this is one of the most underused CSS features for UI work. You'd reach for JavaScript-based canvas or SVG clipping for effects that clip-path handles in four lines of CSS. The mental model shift is: think of clip-path as a stencil, not a mask.
Worth noting: clip-path works on any HTML element, not just images. Cards, buttons, section backgrounds, hero text — all fair game.
The Core Syntax: polygon(), circle(), ellipse(), and inset()
Before you animate anything, you need the four main shape functions down cold. polygon() is the workhorse — you pass it a list of x y coordinate pairs as percentages or lengths. circle(radius at cx cy) is clean for avatar reveals. ellipse() extends circle with separate x/y radii. inset() clips to a rectangle with optional round for border-radius — great for subtle card entrance animations.
Here's the reference you'll keep coming back to:
``css
/* Polygon — diagonal cut, top-left to bottom-right */
.card { clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%); }
/* Circle reveal — starts collapsed, expands to full */
.reveal {
clip-path: circle(0% at 50% 50%);
transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.reveal.visible { clip-path: circle(150% at 50% 50%); }
/* Inset with rounded corners */
.card-enter {
clip-path: inset(20px round 12px);
transition: clip-path 0.4s ease-out;
}
.card-enter.done { clip-path: inset(0px round 12px); }
``
One thing people miss: you can use inset() to fake a wipe reveal from any edge. inset(0 100% 0 0) hides everything by eating from the right. Animate to inset(0 0% 0 0) and you have a left-to-right text reveal with zero JavaScript.
Quick aside: the path() function takes SVG path data, which gives you arbitrary shapes. Browser support landed in all majors by 2023, but animation between two path() values with the same number of commands is finicky. Stick to the named shape functions for anything you need to animate reliably.
Building a Polygon Reveal on Scroll
The circle wipe gets all the attention, but polygon reveals are more flexible for editorial layouts. The trick is defining a start state where all points are collapsed to one edge, then expanding outward. You get this satisfying slide-in-from-corner effect that feels designed without being overbearing.
Here's a React component that wires up an IntersectionObserver to trigger the clip-path transition:
``tsx
import { useEffect, useRef, useState } from 'react';
const PolygonReveal = ({ children }: { children: React.ReactNode }) => {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setVisible(true); },
{ threshold: 0.2 }
);
obs.observe(el);
return () => obs.disconnect();
}, []);
return (
<div
ref={ref}
style={{
clipPath: visible
? 'polygon(0 0, 100% 0, 100% 100%, 0 100%)'
: 'polygon(0 0, 0 0, 0 100%, 0 100%)',
transition: 'clip-path 0.7s cubic-bezier(0.16, 1, 0.3, 1)',
}}
>
{children}
</div>
);
};
``
That easing function — cubic-bezier(0.16, 1, 0.3, 1) — is a spring-ish curve that overshoots slightly and snaps back. It makes the polygon feel like it has weight. A plain ease-out at the same duration will feel flat by comparison.
In practice, you'd combine this with a slight opacity transition too. The clip-path alone can look abrupt on dark backgrounds where the clipped edge is obvious against a light card surface.
Hover Effects: Diagonal Cuts and Shape Morphing
Hover clip-path effects are where this technique gets genuinely fun to build. The idea is simple: on :hover, shift the polygon coordinates. The browser interpolates between them. You have a shape morph in 3 lines of CSS.
A diagonal cut hover on a button — the kind you'd see in cyberpunk or neobrutalism UI — looks like this:
``css
.btn-clip {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.25s ease, background 0.25s ease;
background: #7c3aed;
padding: 12px 28px;
color: white;
font-weight: 600;
}
.btn-clip:hover {
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
background: #6d28d9;
}
``
That 8px offset gives you a parallelogram on hover. Subtle, tactile, zero JavaScript. You can push it further — different corner offsets on each axis, asymmetric shapes — just keep the point count consistent.
One more thing — you can layer multiple elements with clip-path to fake a stacked card effect where the top card reveals the one beneath it on hover. Set the bottom card to a slightly different polygon and let them animate independently. It reads as depth without any actual 3D transform.
Circle Wipe Reveals for Images and Sections
The circle wipe is the most cinematic clip-path animation. You start at circle(0%) and expand to circle(150% at x y) — the 150% overshoots the element bounds so you never see clipping on the far edges during the reveal. The at keyword controls the origin point, so you can have it burst from a click coordinate, from the center, or from a corner.
For an image that reveals on hover:
``css
.img-wrap {
position: relative;
overflow: hidden;
}
.img-wrap img {
clip-path: circle(40px at var(--mx, 50%) var(--my, 50%));
transition: clip-path 0.5s ease-out;
display: block;
width: 100%;
}
.img-wrap:hover img {
clip-path: circle(150% at var(--mx, 50%) var(--my, 50%));
}
``
Pair that with a JS snippet to track mouse position into CSS custom properties and the circle follows your cursor. The effect is used heavily in glassmorphism components where frosted overlays need a reveal moment that feels premium.
Look, the circle wipe is genuinely one of those CSS effects that impresses non-developers disproportionately. It looks like a custom animation library was involved. It's just two clip-path values and a transition.
Performance: What's Safe to Animate
Not all clip-path animations are equal from a performance standpoint. Animating clip-path on an element that also has box-shadow, filter, or paints a large texture will force a repaint on each frame — you lose the compositor-thread advantage.
The safe pattern: wrap your visual element (with gradients, shadows, images) in a plain div, and apply clip-path to the wrapper only. The wrapper itself has no paint cost, so the browser promotes it to its own layer and composites cleanly. Will transform: translateZ(0) force the same behaviour? Yes, but will-change: clip-path is the cleaner signal to give the browser.
Also: avoid animating clip-path on dozens of elements simultaneously without checking with DevTools. In Chrome 120+, the Layers panel will show you exactly what's being composited versus what's repainting. A page with 20 concurrently animating clips can still run well — a page with 20 clips that each trigger layout will not.
That said, on modern hardware and with sensible scoping, clip-path animations are among the cheapest visual effects you can ship. Compared to animating border-radius on a large element, or using SVG <clipPath> with filter effects, pure CSS clip-path interpolation is fast. If you want to see what's possible at the component level, browse the components on Empire UI to see how clip-path fits into larger interaction patterns.
Combining clip-path with CSS Custom Properties for Dynamic Shapes
CSS custom properties open up clip-path in a way that static values can't. You can drive the polygon coordinates from JavaScript by updating --clip-x or --clip-size on a single element and let CSS handle the rest. This pattern is especially useful for scrubbed animations tied to scroll position.
Here's a scroll-driven clip-path that expands a hero section as you scroll down 200px:
``js
const hero = document.querySelector('.hero');
window.addEventListener('scroll', () => {
const progress = Math.min(window.scrollY / 200, 1);
const size = 20 + progress * 130; // 20% → 150%
hero.style.setProperty('--clip-size', ${size}%);
}, { passive: true });
`
`css
.hero {
--clip-size: 20%;
clip-path: circle(var(--clip-size) at 50% 0%);
transition: none; /* JS controls timing, no CSS transition */
}
``
Remove the transition when JS is driving the value — you want frame-accurate control, not a CSS ease fighting your scroll position. Add the transition back only on mouseenter/mouseleave states where you want the easing.
This technique is what powers a lot of the shape-morphing effects you see in high-end marketing sites. The underlying CSS is the same spec you've always had — what changes is *what* updates the custom properties. For more inspiration on combining motion with css-scroll-animations, the patterns map across cleanly.
FAQ
No — you can only animate between the same shape function with the same number of points. polygon() to polygon() works; polygon() to circle() snaps instantly. Use JS to swap a class instead.
Clipped areas are still in the DOM and can receive focus — they're just visually hidden. You'll want to pair clip-path reveals with visibility: hidden or pointer-events: none on the collapsed state to avoid invisible interactive elements.
You're almost certainly triggering paint. Check that the clipped element doesn't have active box-shadow or filter styles. Move those to a child element and clip the parent wrapper instead — that restores compositor-thread promotion.
For CSS-driven animations, yes — the CSS approach skips the overhead of SVG rendering and composites more efficiently. SVG <clipPath> is worth it when you need complex path shapes (like hand-drawn outlines) that the CSS shape functions can't express.