Why clip-path: path() Falls Short for Responsive Design

When clip-path: path() landed in Firefox, it promised a simpler way to create complex clipping shapes on HTML elements without dropping in an SVG or wrestling with polygon(). The idea was enticing: a single <div>, a bit of CSS, and you could have organically shaped elements that breathe and move.

Animated gif. Shows a square breathing in and out - its waistline smoothly contracts and then expands.
Breathing box.

Reality doesn't quite match that vision. The path() function takes an SVG path string as its argument, and that string only accepts unit-less pixel values. Unlike SVG paths, which scale with the viewBox of their parent <svg>, these CSS pixel coordinates do not scale at all. Clip an element of size 35vw with a heart-shaped path, and the heart stays a fixed pixel size while the element itself grows and shrinks with the viewport.

clip-path: path('M256 203C150 309 150 309 44 203 15 174 15 126 44 97 73 68 121 68 150 97 179 68 227 68 256 97 285 126 285 174 256 203')

The result is a clipping shape that doesn't track its element's resizing behavior. That's a critical flaw in an era where responsive design is the baseline, not a nice-to-have. Unless the target element has a fixed pixel dimension, path() is effectively unusable. SVG or a polygon() approximation is still the more reliable choice.

Amelia Bellamy-Royds has proposed two directions for fixing this:

Option 1: Allow calc() values/units inside path data. This would probably be done while extending SVG path syntax in general.

Option 2: Specify viewBox in clip-path declaration, scale path to fit.

Option 1 has more appeal. Option 2 essentially recreates SVG semantics within CSS, and if you're going that far, you might as well include the SVG element itself—browser support will always be stronger. Option 1, however, could make CSS clipping genuinely more capable than embedding inline SVG, not just a poorer stand-in.

The SVG Workaround for a Breathing Box

To understand what path() would need to offer, consider creating a responsive breathing box animation entirely with SVG markup. The viewBox is set with its 0,0 origin at the center of the shape, so the top-left corner coordinates are negative half the viewBox width and height. In SCSS, you define an edge length $l as the smallest viewBox dimension, then construct the path as a square.

<svg viewBox='-75 -50 150 100'>
  <path/>
</svg>

The square's path starts with a move command at the top-left corner, then uses relative vertical and horizontal line commands to trace the four edges. The compiled CSS replaces $l with 100, and the result is a simple square boundary ready to be deformed.

.box {
  d: path('M-50,-50 v100 h100 v-100');
  fill: darkorange;
}

To make the box "breathe," swap the straight edges for quadratic Bézier curves. The control point for each edge is positioned halfway along the edge vertically, and offset horizontally by a quarter of the edge length—either outward to widen the box or inward to narrow it. Doing this for both the left and right edges requires tracking sign conventions carefully, since the right edge travels upward (negative y), and its control point offset flips direction relative to the box's center.

d: path('M#{-.5*$l},#{-.5*$l} 
         q#{-.25*$l},#{.5*$l} 0,#{$l} 
         h#{$l} 
         q#{.25*$l},#{-.5*$l} 0,#{-$l}'); /* swollen box */

d: path('M#{-.5*$l},#{-.5*$l} 
         q#{.25*$l},#{.5*$l} 0,#{$l} 
         h#{$l} 
         q#{-.25*$l},#{-.5*$l} 0,#{-$l}'); /* squished box */

With the two curved-edge states defined, animating between them is straightforward: toggle between the swollen path and the squished path. The only difference between the two states is the sign of the horizontal offset on the control point coordinates. A SCSS mixin keeps this compact at author-time, but the generated CSS still contains two long, near-identical paths.

What a Better path() Would Unlock

If CSS path() could accept units and calc() expressions, you wouldn't need those duplicated path strings. Instead, you'd express the sign of the control point offset as a custom property—say, --sgn—and animate that between -1 and 1 via the Houdini APIs.

div.box {
  width: 40vmin; height: 20vmin;
  background: darkorange;
  --sgn: 1;
  clip-path: path(M 25%,0%
                  q calc(var(--sgn)*-25%),50% 0,100%
                  h 50%
                  q calc(var(--sgn)*25%),-50% 0,-100%);
  animation: breathe .5s ease-in-out infinite alternate
}

@keyframes breathe { to { --sgn: -1 } }

That would let a single path() definition scale fluidly with its element and respond to state changes with purely declarative CSS. The current state of clip-path: path(), locked to fixed pixel coordinates, misses both of those opportunities. It's a feature that works for demos with static, fixed-size boxes—and little else in a modern responsive layout.