Hard cuts ruin good UI. If a shape or panel appears out of nowhere, users feel the jolt. By the end of this article you will build two production-ready patterns that animate clip-path with smooth, reliable transitions: a circular reveal that grows from the center of an image, and a polygon panel that morphs from a tight sliver into a full menu. You will learn when circle(), ellipse(), and polygon() animate cleanly, how to keep polygon point counts compatible, and how to respect reduced motion preferences without losing design intent.
Why Animate clip-path Matters
clip-path is a modern way to reveal and mask content without extra wrappers, complex SVGs, or heavy bitmap assets. It is hardware accelerated on current browsers, supports clean interpolation for several shapes, and composes well with transforms and filters. Animating clip-path yields reveals, wipes, and morphs that look crisp at any resolution. Compared to image sprites or JavaScript-driven canvas effects, you ship less code, gain better control in CSS, and keep the DOM clean. You also gain flexibility: you can switch between a circular reveal and a corner wipe by editing a couple of points rather than rebuilding components.
Prerequisites
You do not need a framework. We will stick to semantic HTML and modern CSS. A basic understanding of units and custom properties will help. If you can read a polygon coordinate list and understand how transitions work, you are ready.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
Here is the complete markup for both demos. Each demo uses a checkbox and label to toggle state with no JavaScript. The first demo reveals an image using a circular clip-path. The second demo morphs a navigation panel from a thin edge into a full overlay. Labels act as buttons and are visually styled; the inputs remain hidden but control state via sibling selectors.
<!-- HTML -->
<section class="stage">
<div class="demo reveal">
<input id="reveal-toggle" class="visually-hidden" type="checkbox">
<label for="reveal-toggle" class="btn">Toggle reveal</label>
<figure class="reveal__frame" aria-label="Decorative image reveal">
<div class="reveal__image" aria-hidden="true"></div>
</figure>
</div>
<div class="demo morph">
<input id="panel-toggle" class="visually-hidden" type="checkbox">
<label for="panel-toggle" class="btn">Toggle panel</label>
<div class="morph__panel" aria-hidden="false" role="region" aria-label="Quick links">
<nav class="morph__menu">
<a href="#">Home</a>
<a href="#">Products</a>
<a href="#">About</a>
<a href="#">Contact</a>
</nav>
</div>
</div>
</section>
Step 2: The Basic CSS & Styling
This baseline sets up a centered stage, theme variables, and a button style. The .visually-hidden class keeps inputs accessible to assistive tech while removing them from layout. Both demos share the same container spacing and font settings to keep focus on clip-path behavior.
/* CSS */
:root {
--bg: #0e0f13;
--panel: #12141b;
--ink: #e7e9ee;
--muted: #a8b2c1;
--brand: #6ee7f0;
--accent: #7c4dff;
--btn: #1f2430;
--radius: 12px;
--gap: 1.25rem;
--w: min(1100px, 92vw);
}
*,
*::before,
*::after { box-sizing: border-box; }
html, body {
height: 100%;
background: radial-gradient(1200px 70% at 50% -30%, #1a1e27, #0e0f13);
color: var(--ink);
font: 500 16px/1.5 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.stage {
width: var(--w);
margin: 6vh auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: calc(var(--gap) * 2);
align-items: start;
}
.demo {
background: var(--panel);
border-radius: var(--radius);
padding: calc(var(--gap) * 1.5);
box-shadow: 0 8px 24px rgb(0 0 0 / 0.25);
}
.btn {
display: inline-block;
margin-bottom: var(--gap);
background: var(--btn);
color: var(--ink);
border: 1px solid #2a3242;
padding: .6rem 1rem;
border-radius: 999px;
cursor: pointer;
user-select: none;
transition: background .2s, color .2s, border-color .2s;
}
.btn:hover { background: #2a3242; }
.visually-hidden {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
Advanced Tip: Keep colors and sizes in custom properties so you can restyle reveals, overlays, and morphs without touching the shape math. Variables also let you animate between presets by swapping values on state selectors.
Step 3: Building the Circular Reveal
The reveal uses clip-path: circle() on the image wrapper. We transition the radius and the center so the circle grows from the middle by default and can drift toward the cursor on hover. The checked state simply raises a CSS variable for the radius. When unchecked, the circle radius returns to zero, hiding the image cleanly.
/* CSS */
.reveal__frame {
position: relative;
inline-size: 100%;
aspect-ratio: 16 / 10;
overflow: hidden;
border-radius: calc(var(--radius) - 2px);
background: #0c0d12;
}
.reveal__image {
inline-size: 100%;
block-size: 100%;
background:
radial-gradient(600px 300px at 20% 10%, color-mix(in oklab, var(--brand) 40%, #000) 0 20%, transparent 60%),
radial-gradient(800px 400px at 80% 80%, color-mix(in oklab, var(--accent) 45%, #000) 0 25%, transparent 60%),
linear-gradient(135deg, #1e2330, #10131a 60%);
/* Initial circular mask shrunk to 0% */
--r: 0%;
--cx: 50%;
--cy: 50%;
clip-path: circle(var(--r) at var(--cx) var(--cy));
-webkit-clip-path: circle(var(--r) at var(--cx) var(--cy));
transition: clip-path 600ms cubic-bezier(.2,.8,.2,1);
will-change: clip-path;
}
/* Expand the reveal on toggle */
#reveal-toggle:checked ~ .reveal__frame .reveal__image {
--r: 140%;
}
/* Nudge the center toward the pointer location on hover for extra polish */
.reveal__frame:hover .reveal__image {
--cx: 70%;
--cy: 35%;
}
How This Works (Code Breakdown)
clip-path: circle() accepts a radius and a center. By keeping both values in variables we can animate either one. The transition targets clip-path, which is animatable for circle(), so the browser interpolates the radius and center smoothly. The radius starts at 0%, which fully clips the content. On :checked, –r jumps to 140%. Using more than 100% ensures the circle clears the corners across aspect ratios.
The hover rule shifts the center to 70% 35%, which adds a directional feel to the expansion. Transitions apply to both the radius and the center, so the movement remains smooth. The gradient stack simulates an image so you do not need external assets. If you want a literal circle mask reference, see the shape recipe for a circle. The underlying principle is identical: you are drawing a circle, then animating its size.
We set will-change: clip-path to hint the compositor that this element will animate that property. This helps keep the transition on the GPU. Avoid placing this hint on many elements, because broad use can waste memory.
Step 4: Building the Polygon Morphing Panel
The panel uses clip-path: polygon() with point lists crafted to keep the same count. Matching the number of points is the golden rule for polygon interpolation. Closed state: a thin sliver on the right edge. Open state: a full rectangle. Since both lists contain eight points in the same point order, the browser can interpolate reliably.
/* CSS */
.morph {
position: relative;
}
.morph__panel {
position: relative;
overflow: clip;
isolation: isolate;
border-radius: calc(var(--radius) - 2px);
background:
radial-gradient(160% 140% at 100% 0%, color-mix(in oklab, var(--brand) 40%, transparent) 0 45%, transparent 55%),
linear-gradient(180deg, #151926, #0f121a);
color: var(--ink);
padding: 1.25rem;
min-block-size: 220px;
/* Define compatible polygons: 8 points each, same order (clockwise) */
--closed: polygon(100% 0%, 100% 0%, 100% 0%, 100% 40%, 100% 100%, 100% 100%, 100% 100%, 100% 0%);
--open: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 100%, 0% 100%, 0% 0%, 0% 0%);
clip-path: var(--closed);
-webkit-clip-path: var(--closed);
transition: clip-path 460ms cubic-bezier(.2,.8,.2,1);
will-change: clip-path;
}
/* Toggle to the open polygon */
#panel-toggle:checked ~ .morph__panel {
clip-path: var(--open);
-webkit-clip-path: var(--open);
}
/* Menu layout inside the panel */
.morph__menu {
display: grid;
gap: .75rem;
max-inline-size: 28ch;
}
.morph__menu a {
display: inline-block;
padding: .5rem .75rem;
border-radius: 8px;
color: var(--ink);
background: color-mix(in oklab, var(--brand) 15%, transparent);
border: 1px solid color-mix(in oklab, var(--brand) 35%, #0b0d12);
text-decoration: none;
transition: background .2s, border-color .2s;
}
.morph__menu a:hover {
background: color-mix(in oklab, var(--brand) 25%, transparent);
border-color: color-mix(in oklab, var(--brand) 55%, #0b0d12);
}
How This Works (Code Breakdown)
polygon() draws straight segments between points in order, then closes the path. Interpolation requires the same number of points and the same winding direction between states. The easiest approach is to decide the maximum point count you will need, then reuse those positions across states, repeating corner coordinates where nothing moves. In the closed shape, most points sit on the right edge to form a thin strip. In the open shape, those same indices fan out to rectangle corners. Because the sequence is consistent, the browser can animate between shapes gracefully.
If you want to shift from a triangular sliver instead, you can encode a triangle shape into polygon points and repeat a vertex to keep counts aligned. For triangle coordinates and proportions, use the recipes such as triangle right and translate those into polygon points. The rule stands: same count, same order, same units.
We set transition: clip-path rather than transitioning a custom property string. clip-path is a real animatable property, which keeps interpolation on the compositor thread. The -webkit-clip-path line helps older Safari builds. overflow: clip avoids painting outside the panel during the morph, which reduces overdraw and visual glitches at the corners.
Advanced Techniques: Adding Animations & Hover Effects
Beyond toggles, you can run keyframes that morph a polygon through multiple states, or drive a circular mask based on interaction. Keep the same point count across keyframes. When building organic shapes, start from a rounded rectangle and perturb points in small amounts to avoid self-intersections.
/* CSS */
/* Organic morphing loop with 8-point polygon */
@keyframes blobber {
0% {
clip-path: polygon(
8% 12%, 92% 8%, 96% 46%, 94% 92%,
62% 96%, 38% 92%, 6% 86%, 4% 44%
);
}
33% {
clip-path: polygon(
14% 8%, 86% 10%, 98% 42%, 90% 90%,
58% 98%, 34% 94%, 4% 82%, 6% 40%
);
}
66% {
clip-path: polygon(
10% 14%, 90% 6%, 94% 48%, 92% 88%,
66% 98%, 36% 90%, 8% 84%, 6% 46%
);
}
100% {
clip-path: polygon(
8% 12%, 92% 8%, 96% 46%, 94% 92%,
62% 96%, 38% 92%, 6% 86%, 4% 44%
);
}
}
/* Apply the animation to the existing panel on hover only when open */
#panel-toggle:checked ~ .morph__panel:hover {
animation: blobber 10s ease-in-out infinite;
}
/* Circle reveal that follows the pointer a bit more aggressively */
.reveal__frame:hover .reveal__image {
--cx: 76%;
--cy: 30%;
--r: 150%;
}
/* Respect reduced-motion */
@media (prefers-reduced-motion: reduce) {
.reveal__image,
.morph__panel {
transition: none !important;
animation: none !important;
}
}
When you want an organic mask, you can also use an approach similar to a CSS blob, then convert its silhouette into polygon points. You do not need many points for a pleasing result. Eight to twelve points often look fluid while keeping interpolation stable.
Accessibility & Performance
Animated masks should add delight without blocking content or input. Focus management, reduced motion, and sensible fallbacks matter.
Accessibility
Because the label toggles a checkbox, screen readers will announce it as a form control. If the control opens a region that contains navigation, a semantic button with aria-expanded and aria-controls on the label’s equivalent would communicate state more clearly. Pure HTML cannot update aria-expanded based on :checked without JavaScript, so consider a button and a small script if this pattern ships to production. Mark decorative layers with aria-hidden=”true”. Add a descriptive aria-label to regions that become visible so their purpose is clear. Respect user motion preferences with prefers-reduced-motion, as shown.
Performance
clip-path animation is performant when you stick to circle(), ellipse(), inset(), and compatible polygon lists. These run on the compositor in current Chromium, Firefox, and Safari. Keep polygon point counts modest. Avoid combining morphing polygons with heavy drop-shadows or large filters on the same element. Apply will-change: clip-path sparingly on elements that actually animate. To prevent flickers on some Safari versions, include both clip-path and -webkit-clip-path declarations, and prefer shapes that do not self-intersect during interpolation.
/* CSS */
/* Fallback if polygon is unsupported */
@supports not (clip-path: polygon(0 0)) {
.morph__panel { clip-path: none; -webkit-clip-path: none; }
}
/* Simple rectangular fallback if transitions are disabled by policy */
@media (prefers-reduced-motion: reduce) {
.morph__panel { clip-path: inset(0); -webkit-clip-path: inset(0); }
}
If you plan a corner wipe that resembles a triangle growing from an edge, keep that triangle stable during the first 10-20% of the animation to avoid clipping the pointer during interactive hovers. For reference geometry and edge alignment, see the building blocks for a triangle right, then translate those to polygon point pairs.
Take These Patterns Further
You built a circular reveal that expands and re-centers smoothly, and a polygon panel that morphs cleanly between two states using compatible point lists. With these patterns you can design page transitions, tooltips that grow from anchors, and contextual highlight effects that feel intentional. Combine precise shape math with your layout, and you have the tools to craft polished interactions that guide attention without noise.