Curvy paths directly in CSS without writing a single SVG path string
The shape() function extends clip-path with real curve commands like curve to, arc to and smooth to, written in readable CSS syntax. Where only an external SVG or a rough polygon approximation used to help, organic, curvy shapes now happen without a build step and without a path editor.
Table of Contents
- 1. What the shape() function is and which problem it solves
- 2. Basic syntax: from, line to, curve to and close
- 3. shape() vs. polygon(): which tool for which job
- 4. Building organic shapes: combining several curve-to commands
- 5. arc to: true circular arcs without a Bézier approximation
- 6. Animating shape(): soft shape transitions without JavaScript
- 7. Browser support and a fallback onto polygon()
- 8. A practical workflow: DevTools editor, design tools and maintainability
- 9. Practical use: campaign areas, badges and decorative dividers
- 10. Summary
- 11. FAQ
1. What the shape() function is and which problem it solves
Until recently, clip-path essentially knew two ways to describe a shape: straight lines through polygon(), or a complete, externally referenced SVG file with a <clipPath> element. For anything needing curves, arcs, or soft transitions, the only option was a detour through SVG, often generated in a design tool and then copied as a path string that is barely readable for humans and even harder to adjust by hand.
shape() closes exactly that gap by introducing real curve commands directly as a CSS function value. Instead of a cryptic SVG d attribute like M10 10 C 20 20, 40 20, 50 10, there is now a readable syntax like curve to 50px 10px with 20px 20px that can be read and understood right in the stylesheet, and combined with CSS values like calc() or custom properties, which is simply not possible with an SVG path string.
2. Basic syntax: from, line to, curve to and close
A shape() declaration always starts with from <start-point> and then lists a sequence of commands that draw a path from that point. line to draws a straight line to the given point, exactly like a vertex in polygon(), while curve to <end-point> with <control-point> draws a quadratic, or with two control points a cubic, Bézier curve. The close command at the end closes the path back to the start point, exactly like Z in SVG.
All coordinates accept the usual CSS length units, percentage values relative to the element's reference box, and even calc() expressions, which makes it possible to describe a shape with a fixed border width of, say, 20 pixels, independent of the element's actual size. That is an advantage a static SVG path string does not offer by nature, because its coordinates get baked in at export time.
/* A triangle with a rounded tip */
.rounded-triangle {
clip-path: shape(
from 50% 0%,
line to 100% 100%,
curve to 0% 100% with 50% 130%,
close
);
}
3. shape() vs. polygon(): which tool for which job
polygon() remains the right choice for anything that genuinely consists of straight edges, such as a chevron, an arrow, or a diagonally cut hero area. The syntax is compact, every point is a simple coordinate, and there is no reason to reach for curve commands that would only end up describing a straight line anyway.
But once an edge is actually meant to look round or organic, the difference becomes clear: recreating a round shape with polygon() means breaking it into many small straight segments, which either looks jagged or requires a large number of coordinate pairs that become nearly unmaintainable by hand. shape() describes the same rounding with a single curve to or arc to command, with exactly the same rendering result but far more readable source.
4. Building organic shapes: combining several curve-to commands
For a freely formed, blob like area, several curve to commands line up one after another, with each control point determining how strongly the curve bulges outward or inward in that section. The practical workflow is usually iterative: set rough vertex points like a polygon first, then replace line to with curve to one at a time and drag the control points live in the browser DevTools shape editor until the silhouette looks organic.
One advantage over an SVG export from a design tool is that every single point and curve stays named and traceable in the source code. Anyone who later wants to adjust only the top edge changes exactly the affected curve to command, instead of searching through a whole machine generated path string trying to guess the right pair of numbers in it.
/* Organic blob shape from four curve segments */
.blob {
clip-path: shape(
from 20% 0%,
curve to 100% 35% with 70% 0%,
curve to 75% 100% with 100% 70%,
curve to 0% 65% with 30% 100%,
curve to 20% 0% with 0% 30%,
close
);
}
5. arc to: true circular arcs without a Bézier approximation
Alongside curve to for Bézier curves, shape() offers its own command, arc to, for true circular and elliptical arcs, specified through an end point, a radius, and a direction (cw for clockwise, ccw for counter clockwise). That is the decisive difference from a Bézier curve, which can only ever approximate a circle: an arc to arc is geometrically exact, which produces visibly smoother results than a roughly estimated curve for rounded corners or circular cutouts.
That makes it possible to build rounded rectangles with a precisely defined corner radius, entirely without border-radius, which is useful whenever the shape is part of a larger clip-path construct and border-radius alone is not enough because a straight edge also needs to be cut elsewhere.
/* Rounded rectangle via four arc-to commands */
.rounded-cutout {
clip-path: shape(
from 0 20px,
arc to 20px 0 of 20px cw,
line to calc(100% - 20px) 0,
arc to 100% 20px of 20px cw,
line to 100% calc(100% - 20px),
arc to calc(100% - 20px) 100% of 20px cw,
line to 20px 100%,
arc to 0 calc(100% - 20px) of 20px cw,
close
);
}
6. Animating shape(): soft shape transitions without JavaScript
Because two shape() values with the same number and order of commands are interpolatable, the browser can animate cleanly between them, for example for a hover state that slightly distorts a blob shape, or a scroll driven transition from a round to an angular silhouette. The only requirement is that the start and end shapes match structurally, meaning the same number of curve to or arc to commands in the same order.
That is a clear advantage over animating an SVG path attribute, which can barely be interpolated meaningfully without an extra JavaScript library like GSAP, because the browser does not automatically map two arbitrary path strings onto each other. With shape(), a plain CSS transition declaration on clip-path is enough, and the browser handles the full interpolation of every individual coordinate.
.blob-interactive {
clip-path: shape(from 20% 0%, curve to 100% 35% with 70% 0%,
curve to 75% 100% with 100% 70%, curve to 0% 65% with 30% 100%,
curve to 20% 0% with 0% 30%, close);
transition: clip-path 0.4s ease;
}
.blob-interactive:hover {
clip-path: shape(from 15% 0%, curve to 100% 25% with 80% 0%,
curve to 80% 100% with 100% 80%, curve to 0% 75% with 20% 100%,
curve to 15% 0% with 0% 20%, close);
}
7. Browser support and a fallback onto polygon()
shape() is newer than polygon(), and support in current browsers is growing but not yet as widespread as the established clip-path functions. The pragmatic approach is a two step fallback: declare a rough polygon() approximation first, which works in every browser, and right after it the finer shape() version, which overrides the previous declaration in browsers that support it.
An @supports query makes that safeguard explicit and prevents a completely angular or missing shape from showing up in older browsers. It matters here that the polygon() approximation is visually close enough to the target shape that the difference does not read as a break for users without shape() support, but at most as a slightly more angular variant of the same shape.
.blob {
/* Fallback: rough polygon approximation for older browsers */
clip-path: polygon(20% 0%, 65% 5%, 100% 35%, 90% 70%, 75% 100%, 35% 95%, 0% 65%, 5% 25%);
}
@supports (clip-path: shape(from 0 0, line to 100% 100%)) {
.blob {
clip-path: shape(from 20% 0%, curve to 100% 35% with 70% 0%,
curve to 75% 100% with 100% 70%, curve to 0% 65% with 30% 100%,
curve to 20% 0% with 0% 30%, close);
}
}
8. A practical workflow: DevTools editor, design tools and maintainability
Current browser DevTools now offer a visual editor for clip-path values that also grabs and drags shape() paths directly in the canvas, similar to a vector tool in a design program. That speeds up fine tuning considerably, because control points can be adjusted with the mouse instead of repeatedly changing numeric values in the editor, while the result immediately lands as a readable CSS value in the stylesheet.
Compared to an SVG path exported from Figma or Illustrator, shape() also stays part of the CSS build process, with no extra asset file, no extra HTTP request, and no separate upkeep between design tool and repository. For a team that adjusts shapes frequently, for example on seasonal campaign pages, that noticeably reduces friction between design and implementation.
9. Practical use: campaign areas, badges and decorative dividers
In a shop context, shape() fits particularly well for seasonal campaign areas with an organic silhouette, for badge and sticker elements with soft instead of sharp corners, and for decorative section dividers that draw a gentle wave instead of a hard edge between two areas. In all three cases, a single shape() declaration replaces an extra SVG asset that would otherwise need separate hosting, loading, and re-exporting with every design adjustment.
It still matters to treat the shape as a purely decorative element here as well: interactive areas like buttons or links should never be made clickable exclusively through a shape() outline, without keeping the underlying click target large enough for keyboard and screen reader use.
| Function | Curves possible | Readability | Typical use |
|---|---|---|---|
polygon() |
No, straight edges only | High, simple coordinates | Chevrons, arrows, diagonal sections |
shape() with curve to |
Yes, Bézier curves | High, named commands | Organic blobs, soft edges |
shape() with arc to |
Yes, true circular arcs | High, named commands | Precise rounding, circular cutouts |
External SVG clipPath |
Yes, arbitrarily complex | Low, generated path string | Very complex, static illustrations |
Mironsoft
Modern CSS, layout architecture and rendering performance
CSS that stays maintainable instead of breaking with every change?
We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.
CSS Audit
Systematically uncovering specificity issues, cascade conflicts and unused selectors.
Architecture Refactoring
Introducing cascade layers, custom properties and design tokens cleanly.
Performance Tuning
Fixing layout thrashing, expensive selectors and rendering bottlenecks.
10. Summary
clip-path shape() in CSS: The Essentials at a Glance
Core idea
shape() brings real curve commands like curve to and arc to directly into clip-path, with no detour through an external SVG path string.
Vs. polygon()
polygon() stays the simpler choice for straight edges, shape() pays off once an edge is actually meant to look round or organic.
Animatable
Two structurally matching shape() values interpolate automatically in the browser, a plain CSS transition is enough for a soft shape change.
Fallback
A polygon() approximation declared before the shape() value plus an @supports query cleanly protect older browsers.