Skip to content

Clip-Path and Masks

When to Use

clip-path hides portions of elements without affecting layout — the clipped area is invisible but still occupies space. Use it for shaped containers, animated reveals, comparison sliders, and creative transitions. Unlike overflow: hidden, clip-path is compositor-accelerated and produces no layout shifts during animation.

Decision

If you need... Use... 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 for clip-path

  • You can animate between polygon() values — but they must have the same number of points. A triangle cannot animate to a rectangle without adding a co-located extra point.
  • You can animate inset() to inset() freely (different values, same type).
  • You cannot animate between shape types (e.g., circle() to polygon()).
  • CSS 2025: the shape() function enables curves with fewer workarounds, but animating to/from shape() still requires matching point counts.

Pattern

Wipe reveal on scroll:

.reveal-wipe {
  clip-path: inset(0 100% 0 0); /* Fully hidden — right edge covers all */
  transition: clip-path var(--duration-slow) var(--ease-emphasized-decel);
}

.reveal-wipe.is-visible {
  clip-path: inset(0 0% 0 0); /* Fully revealed */
}

Diagonal section break:

.diagonal-section {
  clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%);
  /* Top: flat. Bottom: angled down-right */
}

Animated iris reveal (circle expand):

@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%
  );
}

/* Horizontal fade — useful for scrolling carousels */
.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%; /* Controlled by JS */
}

.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

  • Animating between different point-count polygons — browser cannot interpolate; the shape snaps
  • Expecting box-shadow to appear on clipped elements — use filter: drop-shadow() on a wrapper instead
  • Using clip-path for layout-affecting shapes — clip-path hides content but it still occupies its original space; use shape-outside for text wrapping
  • Forgetting the overflow behavior — elements outside the clip region are hidden but still receive pointer events unless you also set pointer-events: none

See Also