css
CSS clip-path: A Working Guide to Shapes, Coordinates & Animation
How clip-path actually works — every basic shape with correct syntax, the coordinate system that breaks on aspect-ratio changes, the real rules for animating shapes, and when to use mask-image or SVG instead.
clip-path doesn’t just hide the parts of an element outside a shape — it makes them un-clickable too. Draw a star over a button and the corners of its old rectangular hitbox stop receiving clicks, hovers, everything. That single fact explains half the “why isn’t this working” bug reports about clip-path, and it’s a good entry point into a property that looks simple and has more sharp edges than it lets on.
What clip-path actually does
clip-path defines a clipping region for an element. Content inside the region paints normally; content outside it is not painted and is not part of hit-testing — pointer events (click, hover, mousedown) don’t reach the clipped-away area. This is different from visibility: hidden or opacity: 0, and different from just “looks cropped.” A circle clipped out of a square element genuinely stops being square as far as the mouse is concerned.
A second effect that surprises people less often but still trips them up: any clip-path value other than none creates a new stacking context on the element, the same way opacity under 1 does. If you’re relying on z-index inside a clipped element interacting with siblings outside it, that’s worth knowing.
Everything below assumes you’re clipping in the browser with real CSS — for a visual way to build these shapes without hand-writing coordinates, the CSS Clip-Path Generator lets you drag points on a live preview (25 presets — triangles, stars, arrows, chevrons, polygons up to a decagon), preview against your own image, and copy the output CSS.
The basic shapes
inset() — rectangles, with rounded corners
inset() crops a rectangle in from the element’s edges. It takes 1–4 offsets (same shorthand logic as margin/padding) and an optional round keyword for corner radii:
.card {
/* 10px in from top/bottom, 20px in from left/right */
clip-path: inset(10px 20px round 12px);
}
Use inset() for anything that’s fundamentally still a rectangle — cropping an image, trimming a hard edge off a card, or animating a “reveal” where content slides out from behind a mask. It’s the cheapest shape to reason about because the geometry stays rectangular.
circle() — radius and position
.avatar {
clip-path: circle(50% at center);
}
.spotlight {
clip-path: circle(closest-side at 30% 40%);
}
circle(<radius> at <position>) — the radius accepts a length, a percentage, or the keywords closest-side / farthest-side (and closest-corner / farthest-corner). closest-side sizes the circle to touch the nearest edge of the reference box from the center point; farthest-side stretches it to touch the farthest edge. at <position> uses the same syntax as background-position — center, top left, or explicit percentages/lengths — and defaults to center if omitted.
ellipse() — two independent radii
.badge {
clip-path: ellipse(130px 80px at 50% 50%);
}
Same idea as circle() but with separate x- and y-radii: ellipse(<rx> <ry> at <position>). The first radius resolves against the reference box’s width, the second against its height — which is exactly the coordinate-system gotcha covered next.
polygon() — arbitrary vertices and fill rule
.chevron {
clip-path: polygon(0 0, 75% 0, 100% 50%, 75% 100%, 0 100%, 25% 50%);
}
polygon() takes a comma-separated list of x y vertex pairs, connected in order and closed automatically back to the first point. It’s the workhorse shape — triangles, arrows, stars, chevrons, cut corners, anything hand-drawn.
An optional leading fill rule controls what happens when the path self-intersects (a star, for instance):
clip-path: polygon(evenodd, /* ...points... */);
nonzero is the default and fills based on winding direction; evenodd alternates fill/no-fill each time a ray crosses an edge. Most simple, non-self-intersecting polygons render identically either way — it only matters once edges cross themselves, like a five-pointed star drawn as one continuous path.
path() and shape() — when you need more than straight lines
/* path(): raw SVG path data, fixed coordinate space */
.blob {
clip-path: path("M20,0 C40,0 60,10 60,30 C60,50 40,60 20,60 C0,60 -10,40 0,20 Z");
}
/* shape(): CSS-native path commands that accept percentages */
.blob-responsive {
clip-path: shape(from 10% 0%, curve to 90% 0% with 50% -20%, line to 90% 100%, close);
}
path() takes an SVG path string with a fill rule, so you get curves and arcs — but the coordinates are fixed lengths from that string, so the shape doesn’t scale with the element. shape() is the newer, CSS-native equivalent: it uses the same curve/line/arc vocabulary but accepts percentages (and calc(), and custom properties), so a shape defined with shape() actually resizes correctly with its element.
Support is the deciding factor here. path() has been supported in every major browser since around 2018–2021 and is safe to use today. shape() is much newer — it only reached support across current Chrome, Firefox, and Safari versions in 2026 — so treat it as a progressive enhancement for curved shapes rather than a baseline default, and verify your target browser versions before shipping it as the only implementation.
The coordinate system people get wrong
Percentages in a basic shape resolve against the element’s reference box — by default the border box, but you can change it by prefixing the shape with a geometry-box keyword:
clip-path: content-box circle(50%); /* percentages resolve against content box */
clip-path: padding-box inset(10%);
clip-path: border-box polygon(0 0, 100% 0, 100% 100%);
clip-path: fill-box circle(40%); /* SVG: uses the object's bounding box */
border-box, padding-box, and content-box apply to regular HTML elements; fill-box (and stroke-box, view-box) matter on SVG elements, where fill-box sizes against the shape’s own bounding geometry rather than a CSS box.
The part that actually causes bugs: 0% 0% is the top-left corner, and the x and y percentages resolve independently — against width and height respectively. They are not the same unit. A point at 50% 50% is the center regardless of aspect ratio, but a point meant to sit on a 45° diagonal only looks like 45° when the element is square.
/* Looks like a clean diagonal cut... only on a square element */
.diagonal {
clip-path: polygon(0 0, 100% 0, 100% 50%, 0% 100%);
}
Resize that element to be wide and short, or tall and narrow, and the “diagonal” visibly bends — because 50% on the y-axis and 100% on the x-axis are measuring completely different lengths. Two real fixes:
- Pin the aspect ratio with
aspect-ratioon the element, so the percentages always resolve against the same proportions. - Use fixed units via
calc()for the axis that needs to stay geometrically true, e.g.calc(100% - 40px) calc(100% - 40px), so the offset doesn’t scale with an axis you don’t want it to.
Animating clip-path
Shapes animate by interpolating each coordinate from the start value to the end value — but only when the two shapes are structurally compatible:
- Same shape function.
circle()cannot interpolate intopolygon(), orellipse()intoinset(). A transition between different shape functions doesn’t animate — it snaps at the halfway point of the transition (a “discrete” animation). - Same vertex count, for polygons. Two
polygon()values only interpolate point-for-point if they have the same number of vertices, in the same order. - Same fill rule, for polygons. Both sides of the transition need matching
nonzero/evenodd, or the values are treated as discrete.
The practical trick when a shape needs to “grow” a point that doesn’t exist in the other keyframe — turning a triangle into a diamond, say — is to pad the simpler shape with duplicate, coincident points so both keyframes have the same vertex count:
.shape {
clip-path: polygon(50% 0%, 100% 100%, 0% 100%, 0% 100%);
transition: clip-path 0.4s ease;
}
.shape:hover {
/* same 4 points, but now the 4th point is distinct — a diamond */
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
The resting state repeats its last point (0% 100%, 0% 100%) purely so the vertex counts line up; visually it’s still a plain triangle. On hover, that duplicated point separates into a real fourth corner and the browser can interpolate every point smoothly instead of jump-cutting. This is the standard workaround wherever you want a shape to appear to gain or lose corners.
Once you have a shape you like, the CSS Animation Generator and Cubic Bezier tools are useful for building the easing and keyframes around it — and if you’re not sure which curve a reveal deserves, CSS easing functions explained covers ease-out vs. ease-in and when each belongs.
clip-path vs the alternatives
| Approach | Edge quality | Cost | Hit-testing | Best for |
|---|---|---|---|---|
clip-path | Hard-edged geometry | Cheap | Clips the hit area to the shape | Precise cuts, cropping, shaped reveals |
mask-image | Soft edges, alpha gradients | More expensive | Doesn’t clip hit area (element box is unchanged) | Fades, vignettes, image-based masks |
border-radius | Simple rounded corners only | Cheapest | Doesn’t affect hit area | Rounded rectangles, pills, avatars |
overflow: hidden on a shaped wrapper | Depends on wrapper shape | Cheap | Standard box hit-testing | Clipping children to a parent’s bounds |
SVG <clipPath> (with clipPathUnits) | Hard-edged, arbitrarily complex | Cheap to render, more setup | Clips the hit area | Reusable or very complex geometry shared across elements |
mask-image is the right tool the moment you want a gradient fade instead of a crisp line — clip-path can’t do soft edges at all, since a point is either inside the shape or outside it. For simple rounded corners, skip clip-path entirely and use border-radius; it’s cheaper and doesn’t touch hit-testing. For a rounded shape with continuous, “squircle” curvature rather than circular arcs, the Squircle Generator and Border Radius Generator cover that without clip-path at all. And if you’re clipping the same complex shape onto many elements, an SVG <clipPath> defined once and referenced by id (with clipPathUnits="objectBoundingBox" if you want it to scale with each target) is more maintainable than repeating a polygon() value everywhere.
Gotchas
- Clipping doesn’t shrink layout. The element still occupies its full, unclipped box for the purposes of document flow — margins, adjacent elements, and floats all behave as if the box were its original rectangle. Clip a triangle out of a 200px-tall element and it still takes up 200px of vertical space; nothing reflows around the visible shape.
box-shadowandoutlineget clipped away too. Both are painted as part of the element’s box, soclip-pathcuts them off along with everything else — a shadow “outside” a star-shaped clip simply isn’t drawn. If you need a shadow that follows a clipped shape, apply the shadow to a wrapper element and the clip to an inner one, or fake the shadow with a second, blurred copy of the shape behind it.- Clipped overflow inside scroll/sticky containers can behave unexpectedly, since
clip-pathcreates a stacking context — a clippedposition: stickychild can end up clipped by its own new context in ways that are easy to misdiagnose as a sticky-positioning bug rather than a clip-path one.
Accessibility note
Never let a shape clip away text that’s meant to be read — a clipped headline that loses a descender or a clipped button label that loses half its text is a readability failure, not just a visual one. And because clipping changes the actual hit area, double-check that shaped buttons and links are still fully clickable across their visible area — a clip-path that visually shows an icon but leaves an oddly-shaped hitbox is a real usability bug for mouse and touch users alike, not just a screen-reader concern.
Browser support, in short
The basic shapes — inset(), circle(), ellipse(), polygon() — along with path(), have been supported across all major browsers for years and are safe to use without fallbacks. shape() is the one exception: it only became broadly available across current Chrome, Firefox, and Safari in 2026, so verify your minimum supported browser versions before relying on it as anything other than a progressive enhancement.
Build the shape visually, verify the coordinates, then paste it in — the CSS Clip-Path Generator handles the vertex math and gives you the exact CSS string, presets included, so you’re not hand-tuning polygon points in a text editor.