clip-path: path() has always carried a tax. The argument is a single quoted string of SVG path syntax, which means no CSS units, no calc(), no custom properties, and no percentages — every coordinate is a unitless number in the element’s own coordinate space. A clipped shape that needed to respond to layout had to be regenerated in JavaScript or duplicated across breakpoints.
shape() removes that tax. It reached Baseline Newly Available in February 2026, and it expresses the same geometry as path() using ordinary CSS values.
Support and Scope
Chrome shipped shape() in version 135 and Safari in 18.4, both during 2025. Firefox completed the set in February 2026, which is when the feature crossed into Baseline Newly Available. Firefox’s exact stable version isn’t consistently reported across the compatibility trackers, so treat “current Firefox” rather than a specific number as the floor.
shape() is a <basic-shape> value, so it goes anywhere that type is accepted. In practice today that means clip-path and offset-path. MDN also lists border-shape, which is worth flagging: border-shape itself is still experimental, available only in Chrome Canary behind the experimental-web-platform-features flag, with no Firefox or Safari implementation. The shape() syntax is ready; that particular consumer of it is not.
The Formal Syntax
shape() = shape( [<fill-rule>]? from <position>, <shape-command># )
Every shape starts with a from clause giving the first point, measured from the top-left of the reference box, followed by a comma-separated list of commands. The optional <fill-rule> is nonzero (default) or evenodd.
One sharp edge: fill-rule is not supported in offset-path, and including it invalidates the property outright. The declaration doesn’t degrade — it drops.
A triangle reads about as plainly as it can:
.notice {
clip-path: shape(from 0% 0%, line to 100% 0%, line to 50% 100%, close);
}
The Command Set
Seven commands cover the whole surface.
move— repositions without drawing, which is how you get multiple subpaths in one shapeline— a straight segment to a coordinate pairhline/vline— single-axis segments taking one<length-percentage>instead of a paircurve— Bézier curves:curve [by|to] <end-point> with <control-point> [/ <control-point>], where one control point gives a quadratic and two give a cubicsmooth— a Bézier that inherits its incoming tangent from the previous curve, so continuity is automatic rather than hand-computedarc— elliptical arcs:arc [by|to] <coords> of <rx> [<ry>] [cw|ccw] [large|small] [rotate <angle>]close— a straight segment back to thefrompoint
smooth deserves attention. In SVG path syntax the shorthand curve commands exist for the same purpose, but you still reason about reflected control points. Here the reflection is the command’s whole job, which removes the most common source of visible kinks in hand-authored curves.
by vs to: The Distinction That Matters Most
Every drawing command takes either by or to, and mixing them up is the fastest way to produce a shape that looks almost right.
| Keyword | Coordinates are | Measured from |
|---|---|---|
to | absolute | top-left of the reference box |
by | relative | the current command’s starting point |
by is what makes shapes composable. A rounded tab, a notch, or a repeated scallop can be written as a sequence of relative offsets that stays correct wherever the previous command left the pen.
.ticket {
clip-path: shape(
from 0 8px,
arc by 0 -16px of 8px cw,
hline to calc(100% - 8px),
arc by 16px 0 of 8px cw,
vline to calc(100% - 8px),
hline to 0,
close
);
}
Note calc() inside the coordinates. That single capability is most of the reason to migrate off path().
Control Points Get Their Own Reference Frame
curve and smooth accept from start, from end, or from origin on a control point, which changes what that control point’s coordinates are relative to:
#shape {
clip-path: shape(
from center left,
curve by 200px 0 with 50% -50% from start / 50% 0 from origin,
smooth to center with 50% 100% from origin,
close
);
}
This is the part of the syntax with no path() equivalent. Percentage-based control points anchored to the segment’s own start let a curve keep its character as the element resizes, instead of flattening or overshooting the way fixed coordinates do.
Where It Pays Off
Three cases justify a rewrite of existing path() declarations:
Shapes that track the box. Any clip whose geometry should follow padding, container size, or a custom property can now do so directly. Mixing % with px in the same shape — a fixed 8px corner radius on a fluid-width element — is not expressible in path() at all.
Animated clips. clip-path interpolates between two shape() values when the command lists match in structure, so shape-to-shape transitions no longer need a JavaScript tween or a pair of hand-matched SVG strings.
Motion paths. offset-path: shape(...) describes a track in CSS units, so an element animating along a curve can follow a path defined in percentages of its container. Just keep fill-rule out of it.
For genuinely static decorative geometry that never responds to layout, path() still works and there’s no reason to churn it. The migration argument is specifically about responsiveness — the same argument that applied to anchor positioning replacing JavaScript placement libraries. The geometry was always expressible; what was missing was the ability to express it in the units the rest of the stylesheet already speaks.



