CSS shape(): responsive clipping without fixed coordinates (goodbye path() and fragile SVG masks)
webdevelopment August 6, 2026 · Mintec

CSS shape(): responsive clipping without fixed coordinates (goodbye path() and fragile SVG masks)

shape() is the new CSS clip-path function that accepts complex curves with real CSS units: percentages, rem, calc(), and even mixed units that keep a curve's thickness constant while the shape scales. It hit Baseline in February 2026 (Chrome 135+, Safari 18.4+, Firefox). Here's what changes for organic image and video crops, with the decision framework we use on Mintec projects.

CSS shape(): responsive clipping without fixed coordinates (goodbye path() and fragile SVG masks)

Yes: since February 2026 there is a CSS function that clips images and video into organic curves that genuinely scale with the element — it's called shape(), it's part of clip-path, and it kills the two workarounds we've used until now: path() with fixed pixel coordinates, and SVG masks that break on mobile. Chrome 135 and Safari 18.4 shipped it in April 2025; Firefox completed support in early 2026, and the platform declared it Baseline in web.dev's February 2026 digest. In other words: safe for production, no excuses.

We write this from the work, not the theory. In recent Mintec projects, organic clipping — blob masks, wavy edges, curved silhouettes over product images and video — was the #1 source of responsive bugs that never showed up on desktop. Every time a designer exported an SVG mask and the developer wired it with clip-path: url(#...), it looked perfect on the artboard and deformed on the mobile viewport. shape() solves exactly that problem.

The problem: the false choice before shape()

Before shape(), if you wanted a curved clip (not a straight-edged polygon), you had three options, and all three lied:

  • polygon(): genuinely responsive (accepts percentages), but straight lines only. Any curve became 20 points that crunched when scaling.
  • path(): real curves with SVG commands, but fixed pixel coordinates only. A path that looked right at 600px fell apart at 320px.
  • SVG <clipPath> masks: they scale, but only uniformly — you couldn't say "the shape grows but the wave thickness stays the same." And you dragged along an SVG file, a viewBox, and a url(#...) reference that broke whenever the designer touched the file.

The practical result: clips that required JavaScript (resize listeners recalculating paths), hand-pasted inline SVG, or simply "the blob looks different on mobile, ship it anyway."

What shape() is: SVG paths with CSS units

shape() is a <basic-shape> function that accepts SVG-path-like commands — from, line to, hline to, vline to, curve to, smooth to, arc to, close — with the difference that changes everything: every coordinate accepts any CSS unit, including percentages, rem, calc() and custom properties.

The canonical example comes from the Chrome team's article, Use shape() for responsive clipping: a flag with curved borders that looked like this with path():

.flag {
  clip-path: path("M 0 20 C 25 0 75 40 100 20 V 80 C 75 100 25 60 0 80 z");
}

Everything in pixels, frozen at the size it was drawn for. The same flag with shape():

.flag {
  clip-path: shape(
    from 0% 20%,
    curve to 100% 20% with 25% 0% / 75% 40%,
    vline to 80%,
    curve to 0% 80% with 75% 100% / 25% 60%,
    close
  );
}

Same commands, same curve — but now in percentages. The clip scales with the element. And here's the interesting part: units can be mixed inside the same shape. Want the flag to scale but keep the wave at a constant 20px height?

.flag {
  clip-path: shape(
    from 0% 20px,
    curve to 100% 20px with 25% 0% / 75% 40px,
    vline to calc(100% - 20px),
    curve to 0% calc(100% - 20px) with 75% 100% / 25% calc(100% - 40px),
    close
  );
}

That — a % and a px coexisting in the same coordinate — was literally impossible with path() and SVG <clipPath>. It's the feature that kills the organic-mask-on-mobile problem.

Animated clipping without JavaScript

Because shape() lives in CSS, it can be animated with custom properties registered via @property. The Chrome team shows a waving flag in pure CSS: register --wave-height as <length>, animate it with @keyframes, and let clip-path consume it:

@property --wave-height {
  syntax: "<length>";
  inherits: false;
  initial-value: 40px;
}

@keyframes wave {
  from { --wave-height: 0px; }
  to   { --wave-height: 180px; }
}

.flag {
  animation: wave 1s infinite alternate;
  clip-path: shape(
    from 0px var(--wave-height),
    curve to 100% var(--wave-height) with 25% 0px / 75% calc(var(--wave-height) * 2),
    close
  );
}

Animating an organic mask used to mean JavaScript manipulating an SVG path frame by frame. Now it's a @keyframes block and a custom property. For the kind of animated pieces we produce on the media production side of Mintec, this cuts development time and, more importantly, removes the JS layer that broke every time the client changed the hero copy.

Comparison: which technique to use (and when)

TechniqueComplex curvesScales with elementMixed unitsCSS-animatableStatus in 2026
polygon()No (straight lines only)YesYes (%, calc())YesBaseline, ideal for simple shapes
path()YesNo (fixed pixels)NoNo (JS)Legacy for responsive work
SVG <clipPath>YesUniform scale onlyNoHardLegacy; useful as fallback
shape()YesYesYes (%, px, rem, calc(), variables)Yes (@property)Baseline since Feb 2026

The framework we apply on projects:

  1. Straight edgespolygon(). Simple, Baseline, zero friction.
  2. Organic curves that must scaleshape(). This is 90% of product blob masks.
  3. Constant curve thickness while the shape growsshape() with mixed units. The case no other method solves.
  4. Legacy browsers → fallback with @supports (clip-path: shape(from 0 0, line to 100% 0, close)) and a polygon() or SVG mask before the modern block.
  5. Soft edges (feathering)mask-image with gradients, not clip-path. A hard clip doesn't blur edges.

What we did on a real e-commerce build

In a recent e-commerce redesign, the product image blocks used blob masks exported as SVG. The problem appeared exactly as documented in CSS-Tricks' clip-path reference: what looked right on desktop wasn't what shipped on mobile, because the mask scaled the curve thickness along with everything else. We migrated the six blocks to shape() with mixed units: the blob body scales with %, the wave thickness stays fixed in px. Result: ~40 lines of inline SVG deleted, one resize listener that recalculated paths in JavaScript deleted, and the mobile bug gone — because the clip no longer depends on a specific size.

On a media client's site we applied the same approach to video: clip-path: shape(...) directly on a <video> element in the hero, achieving an organic silhouette that previously required rendering the video with a mask in the editing suite. The clip happens at paint time — it doesn't touch layout or decoding, the same video performance principles we've documented. Combine that with the :playing and :paused pseudo-classes and @starting-style for entrance animations, and you have a clipped, animated video hero with zero JavaScript.

Our take (no hedging)

path() is still the wrong answer for responsive clipping, and the "designer exports an SVG mask" pipeline is legacy: every time you see clip-path: path("M 0 0 ...") or clip-path: url(#mask) in a new project, that's freshly minted technical debt. shape() isn't a cosmetic replacement — it changes where the responsibility for the clip lives: from the frozen SVG file to the CSS that already scales with your design. It's the same shift we saw with contrast-color() and reading-flow: the platform absorbing manual work we used to do with tools from another paradigm.

Two honest warnings. First, today shape() applies to clip-path; the spec also covers offset-path, but in production, use it for clipping. Second, don't clip content that needs to stay legible: masking text was a bad idea with SVG and it's still a bad idea with shape() — clipping fixes geometry, not contrast. And if you're animating, register the variables with @property instead of animating the whole property: animating a full clip-path: shape() is expensive; animating one registered <length> is cheap.

Next time a designer sends you a blob exported from Figma, ask for the intent, not the file: what curve, what thickness, what proportion. With that you write a five-line shape() that works in every viewport.

Frequently Asked Questions

What is the shape() function in CSS?

shape() is a CSS function of the <basic-shape> type that defines clipping paths for clip-path using SVG-path-like commands (from, line to, curve to, arc to, close) but with any CSS unit: percentages, rem, px, calc() and custom properties. Unlike path(), which only accepts fixed pixel coordinates, shape() creates organic shapes that scale with the element.

Is shape() supported in all browsers?

Yes — since February 2026 it is Baseline: Chrome 135+ and Safari 18.4+ have supported it since April 2025, and Firefox joined in early 2026. For older browsers you use progressive enhancement with @supports and a fallback clip (for example, polygon() or an SVG mask).

What is the difference between shape() and path() in clip-path?

path() accepts an SVG command string with fixed pixel coordinates: when the element changes size, the clip doesn't adapt unless you recalculate it. shape() accepts the same curves but with relative units (percentages, container units, calc()), and lets you mix units inside the same shape — for example, scaling the body of the shape with % while keeping a curve's thickness fixed in px.

Related Articles