Skip to content

Clip-Path and Masks

When to Use

Use clip-path for shaped containers, animated reveals (wipes, sweeps, iris), comparison sliders, and creative transitions. It is compositor-accelerated and produces no layout shifts. Use mask-image when you need soft edges instead of hard clip boundaries.

Decision

Situation Choose Why
Animate a reveal (wipe, sweep, iris) clip-path from hidden to visible state Compositor-only, smooth 60fps
Reveal content on scroll clip-path + IntersectionObserver class toggle Cleaner than opacity-fade for directional reveals
Diagonal or angled section break clip-path: polygon() on the section No extra pseudo-elements needed
Complex SVG shape mask clip-path: url(#svg-clippath) Arbitrary shapes, curves, text masks
Shadow on a clipped shape filter: drop-shadow() on the parent box-shadow is clipped; drop-shadow follows the shape
Fade content at an edge mask-image with gradient Clip-path creates hard edges; mask creates soft ones

Animation Rules

  • Animate between polygon() values — must have the same number of points
  • Animate inset() to inset() freely
  • Cannot animate between shape types (e.g., circle() to polygon())

Pattern

Wipe reveal on scroll:

.reveal-wipe {
  clip-path: inset(0 100% 0 0);
  transition: clip-path var(--duration-slow) var(--ease-emphasized-decel);
}
.reveal-wipe.is-visible { clip-path: inset(0 0% 0 0); }

Diagonal section break:

.diagonal-section { clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%); }

Animated iris reveal:

@keyframes iris-open {
  from { clip-path: circle(0% at 50% 50%); }
  to   { clip-path: circle(150% at 50% 50%); }
}
.iris-reveal { animation: iris-open var(--duration-slow) var(--ease-emphasized-decel) both; }

Mask-image for soft edge fade:

.fade-bottom {
  mask-image: linear-gradient(to bottom, black 0%, black 70%, transparent 100%);
}
.fade-sides {
  mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%);
}

Clip-path comparison slider:

.comparison { position: relative; --divider: 50%; }
.comparison .before { position: absolute; inset: 0; clip-path: inset(0 calc(100% - var(--divider)) 0 0); }
slider.addEventListener('input', (e) => {
  comparison.style.setProperty('--divider', `${e.target.value}%`);
});

Common Mistakes

  • Wrong: Animating between different point-count polygons — Right: Browser cannot interpolate; the shape snaps
  • Wrong: Expecting box-shadow on clipped elements — Right: Use filter: drop-shadow() on a wrapper instead
  • Wrong: Using clip-path for layout-affecting shapes — Right: Clipped content still occupies its original space; use shape-outside for text wrapping
  • Wrong: Forgetting pointer events — Right: Elements outside the clip region are hidden but still receive pointer events; add pointer-events: none if needed

See Also