Clip Path
Clips an element and everything inside it to a shape. Check out the MDN for how to use.
The shape applies to the element as one image, so children, text, borders and background are all cut by the same edge — and, unlike mask-image, so are pointer events: a click that lands outside the shape passes through to whatever is behind it, because a clip changes the element’s shape rather than painting over it.
.stage { width: 300px; height: 200px; margin-top: 16px; background-image: linear-gradient(135deg, gold, crimson); clip-path: circle(45%); }
Values
Every CSS basic shape is supported, on its own or with a geometry box that says what it is measured against. url(), which points at an SVG <clipPath>, is not.
inset(), rect() and xywh()
clip-path: inset(10px);
clip-path: inset(10px 20px);
clip-path: inset(10% 20% 30% 40%);
clip-path: inset(0 round 24px);
clip-path: inset(0 round 40px 10px / 20px 5px);
clip-path: rect(10px 180px 70px 40px);
clip-path: rect(20px auto auto 30px round 8px);
clip-path: xywh(20px 10px 100px 40px);
clip-path: xywh(10% 20% 60% 60% round 16px);
Three ways to write a rectangle.
inset() takes one to four lengths or percentages in from the top, right, bottom and left edges — the same shorthand as padding.
rect() takes exactly four, and each is where that edge is, measured from the box’s top-left corner: rect(10px 180px 70px 40px) on a 200px-wide box leaves a rectangle 40px from the left and 20px from the right. auto is the box’s own edge, so rect(auto auto auto auto) is the whole box.
xywh() takes exactly four as well — an x and y offset from the top-left corner, then a width and a height, which is how a layout tool exports a rectangle. A negative size is clamped to zero rather than mirroring the rectangle.
All three accept an optional round, which takes the whole border-radius grammar including the / that splits the horizontal radii from the vertical ones. Radii that together overflow a side are scaled down as a set, so inset(0 round 100%) is a stadium rather than four overlapping quarter-ellipses.
circle() and ellipse()
clip-path: circle(50px);
clip-path: circle(40% at 25% 75%);
clip-path: circle(farthest-side at left top);
clip-path: ellipse(50px 30% at center);
A radius per axis (one for a circle, two for an ellipse), optionally followed by at and a position in the background-position grammar. The radius may also be closest-side or farthest-side, which measure to the nearest or furthest edge from the centre — of all four sides for a circle(), which has a single radius, and per axis for an ellipse(). closest-side is the default.
A percentage radius on a circle() resolves against the box’s diagonal rather than either side, which is what keeps the shape round on a rectangle.
polygon()
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
clip-path: polygon(evenodd, 0 0, 100% 0, 100% 100%, 0 100%);
Three or more comma-separated vertices, each an x y pair of lengths or percentages from the box’s top-left. An optional nonzero (the default) or evenodd fill rule may come first; the two differ only where the outline crosses itself.
There is no practical limit on the number of vertices — see how a shape reaches the GPU for what changes past sixteen of them.
path()
clip-path: path('M 0 0 L 100 0 L 100 50 Z');
clip-path: path('M 100 0 A 100 100 0 1 1 100 200 A 100 100 0 1 1 100 0 Z');
clip-path: path(evenodd, 'M 0 0 C 40 0 80 40 80 80 Z');
An SVG path string, in the quotes CSS asks for. The whole d grammar is read: M, L, H, V, C, S, Q, T, A and Z, each in its relative lowercase form as well, with repeated argument lists, implicit linetos after a moveto, and packed arc flags — so path data straight out of a design tool works, minified or not.
Coordinates are lengths in px, measured down from the box’s top-left corner as SVG measures them. Percentages are not part of the grammar; shape() is the function that has them. An optional fill rule may come first, comma-separated from the string.
The string may hold several subpaths, and each is filled as though it were closed whether or not it says Z. The fill rule then decides what a subpath inside another one does: nonzero fills both, and evenodd makes the inner one a hole.
shape()
clip-path: shape(from 0 0, line to 100% 0, line to 100% 100%, close);
clip-path: shape(from 10px 10px, line by 30px 0, line by 0 30px, close);
clip-path: shape(from 0 50%, curve to 50% 0 with 0 0, curve to 100% 50% with 100% 0, close);
clip-path: shape(evenodd, from 50% 0, arc to 50% 100% of 25% large cw, close);
The same outlines as path(), written in CSS instead of SVG — which buys percentages, calc() and var(), none of which a path string can carry.
A from gives the starting point, and each comma-separated command after it moves on from there. to is absolute and by is an offset from the current point:
| Command | Draws |
|---|---|
move [to|by] <x y> | starts a new subpath |
line [to|by] <x y> | a straight line |
hline [to|by] <x> / vline [to|by] <y> | a line along one axis |
curve [to|by] <x y> with <cx cy> | a quadratic curve |
curve [to|by] <x y> with <cx cy> / <cx cy> | a cubic curve |
smooth [to|by] <x y> [with <cx cy>] | a curve whose first control point mirrors the last one |
arc [to|by] <x y> of <rx> [<ry>] [large|small] [cw|ccw] [rotate <angle>] | an elliptical arc |
close | closes the subpath |
arc defaults to small and ccw, and to a circular arc when given one radius. As in path(), y is measured down from the box’s top-left corner, and an optional fill rule may come first.
Geometry boxes
clip-path: content-box;
clip-path: padding-box circle(30px);
clip-path: circle(30px) margin-box;
A geometry box says which of the element’s boxes the shape is measured against, and may come before or after the shape. border-box is the default, and padding-box, content-box and margin-box are the element’s box minus its border, minus its border and padding, and plus its margin. The three SVG boxes are accepted and resolve the way they do on any element with no SVG geometry: fill-box to the content box, stroke-box and view-box to the border box.
The box on its own is a shape — the box itself — so clip-path: content-box clips the element to its content box, cutting off its own padding and border.
Notes
- Lengths and percentages are kept as written and resolved against the box each frame, so a percentage shape follows the element as it resizes, and
calc()works inside one. Ashape()’s coordinates takevar()as well. - A bare geometry box does not pick up the element’s
border-radius; it clips to a plain rectangle. - Two shapes of the same kind interpolate, so a shape can be animated or transitioned. A
path()or ashape()interpolates with another whose commands line up one for one — same command, sametoorby— which is the same rule CSS applies. Different kinds, a change of extent keyword, a change of vertex count, a change of geometry box and a change of fill rule are all discrete, again exactly as in CSS. - The edge is antialiased, so a diagonal or a curve comes out smooth rather than stepped.
- Clipping happens after
filterand beforeopacity, which is the order CSS composites them in: adrop-shadow()is clipped along with the element that cast it. - Clipping puts the element through the same offscreen capture
filterandmix-blend-modeuse, so it also creates a stacking context the wayisolation: isolatedoes. clip-path: noneis the initial value and costs nothing — the capture is torn down and the element goes back to drawing in place.
How a shape reaches the GPU
Worth knowing, because it is the one place a complex shape costs something a simple one does not.
A rounded box and an ellipse are a couple of uniforms each, and the fragment shader evaluates them directly as signed distances — which is what keeps a circle round on an oblong element and its edge antialiased from the distance’s own gradient. A ring of up to sixteen points travels the same way, walked per fragment.
Past sixteen points — which any flattened curve is — the shape is rasterized to a coverage mask once, at the resolution the element is captured at, and the composite samples that instead. So a path() with fifty curves in it costs the same per frame as one with two, and the cost of the shape itself is paid when it changes rather than every frame. What it does cost is a texture per element and a re-rasterization when the element resizes, and beyond 1024 device pixels a side the mask is stretched rather than grown, which softens the edge slightly on a very large element.
Hit testing never goes through the mask: a pointer is tested against the flattened outline itself, so it agrees with the pixels to the point rather than to the texel.