CSS Shapes Beyond border-radius: polygon(), inset() and path()
Stop rounding corners and start cutting shapes. This guide covers clip-path polygon(), inset(), and path() with real examples you can ship today.
Why border-radius Isn't Enough Anymore
You've been reaching for border-radius since about 2011 and it's served you well. Rounded cards, pill buttons, circular avatars — all great. But your designs are starting to look like every other SaaS dashboard, and you know it. The problem isn't the radius itself, it's that you've stopped there.
CSS clip-path has been production-safe since Chrome 55, Firefox 54, and Safari 10.1 — we're well past the experimental phase. In 2026, there's no good reason to avoid it. You get hard geometric shapes, diagonal cuts, custom polygons, and even SVG-path clipping with zero JavaScript and zero extra DOM nodes. Just CSS.
That said, the syntax trips people up because it feels backwards. You're not drawing a shape — you're defining which region of the element is *visible*. Everything outside the clip is hidden, but it still occupies space in the document flow. That distinction matters for layout. Worth noting: clip-path clips the painted area but not the element's box model, so margins and hit targets remain rectangular unless you explicitly account for that.
This article walks through three functions you should actually know: polygon(), inset(), and path(). Each has a sweet spot. Mixing them with the styles available in Empire UI's component library is where things get genuinely interesting.
polygon() — The Workhorse
polygon() takes a list of x/y coordinate pairs and connects them into a closed shape. It's the most flexible of the three and the one you'll use most. Each coordinate is expressed in percentages (relative to the element's own dimensions) or absolute length values. Percentages are almost always the right call — they let the shape scale with the element.
Here's a simple chevron-cut card that slices the bottom-right corner at a 24px diagonal:
.card {
clip-path: polygon(
0% 0%,
100% 0%,
100% calc(100% - 24px),
calc(100% - 24px) 100%,
0% 100%
);
}Five points. Top-left, top-right, then we pull the bottom-right corner 24px inward twice to create the cut, then bottom-left. Dead simple once you see the pattern. You can animate this too — clip-path is animatable when both keyframes use the same number of points, which is one of the more elegant properties of the polygon function.
Honestly, the most practical use case is diagonal section dividers. You've seen them everywhere — hero sections with a slanted bottom edge, pricing tables with clipped headers. Instead of fighting with SVG or negative margins, you do this:
.hero {
clip-path: polygon(0 0, 100% 0, 100% 88%, 0 100%);
padding-bottom: 80px; /* compensate for hidden area */
}Quick aside: the padding-bottom compensation is something beginners always forget. Because the clipped area still occupies box-model space, the next element doesn't automatically slide up. Add padding equal to roughly the depth of your diagonal cut.
inset() — The Underrated One
inset() clips to a rectangle defined by four inset values from each edge — top, right, bottom, left. It's like border-radius for clip-path, and it's the one most devs skip because rectangles sound boring. They're not.
The killer feature is the optional round keyword:
.card {
clip-path: inset(0 0 0 0 round 16px 16px 0 0);
}That clips a perfectly rectangular element but rounds only the top two corners to 16px. border-radius can already do that, sure — but here's the difference: you can *animate* between inset() values without reflowing the layout. Reveal animations that used to need JavaScript clip masks now take three lines of CSS. This is actually useful for skeleton loaders, card hover states, and staggered reveal sequences.
In practice, inset() shines hardest when you need to clip an image or a video to a cropped sub-region without altering the element's actual dimensions. Cropping a 300x200 image to show only a 180x120 center region:
.photo-crop {
clip-path: inset(20px 60px 20px 60px round 8px);
}One more thing — if you're building UI that changes shape on hover or focus, inset() transitions are GPU-composited in Chromium as of version 112. You get smooth 60fps shape transitions for free. No will-change hack required.
path() — SVG Power in a CSS Property
When polygon and inset run out of road, path() picks up the slack. It takes a literal SVG path data string and uses it as the clip region. Curves, arcs, complex organic shapes — anything an SVG <path> can express, you can clip to it.
.blob {
clip-path: path('M 50 0 C 100 0, 120 30, 100 60 C 80 90, 20 90, 0 60 C -20 30, 0 0, 50 0 Z');
}The trade-off is that path() coordinates are absolute pixel values, not percentages. That means the shape is fixed-size and won't scale with the element. This is the biggest footgun with path() — you define it for a 400x400px element and then resize the element and the clip goes completely wrong. For scalable organic shapes you're usually better off with an inline SVG <clipPath> element, which does support userSpaceOnUse and objectBoundingBox coordinate systems.
That said, path() is perfect for fixed-size decorative elements like avatars, icon crops, or badge shapes where the dimensions are known and controlled. Pair it with a CSS custom property to make the path swappable:
:root {
--badge-path: 'M 60 0 L 120 20 L 120 80 L 60 100 L 0 80 L 0 20 Z';
}
.badge {
width: 120px;
height: 100px;
clip-path: path(var(--badge-path));
}Look, browser support for path() in clip-path landed fully in Firefox 97 (2022) and has been in Chromium since 88. Safari completed support in 15.4. You can use it without a fallback for anything except extremely old devices.
If you're building UI with neobrutalism or cyberpunk aesthetics — angular, deliberately rough shapes — path() combined with polygon() gives you enough raw control to build pretty much anything. Check out how Empire UI's neobrutalism and cyberpunk style hubs use hard geometric clipping as a core part of their visual language.
Animating clip-path: What Actually Works
CSS can animate clip-path between two values when the shape function matches and the point count is equal. That's the whole rule. polygon(4 points) to polygon(4 points) — smooth. polygon(4 points) to polygon(6 points) — instant jump, no interpolation.
Here's a hover reveal that morphs a rectangle into a diagonal cut, which works great for cards or image reveals:
.card {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.card:hover {
clip-path: polygon(0 0, 100% 0, 100% 85%, 85% 100%, 0 100%);
}Four points on each state — it interpolates cleanly. The cubic-bezier(0.4, 0, 0.2, 1) is Material Design's standard easing curve and it feels natural for shape morphs. Worth experimenting with cubic-bezier(0.34, 1.56, 0.64, 1) if you want a subtle springy overshoot at the end of the reveal.
For keyframe animations — say, a pulsing organic blob — you need identical point counts on every keyframe. Fourteen points is enough resolution for a smooth organic shape, and you can interpolate all fourteen in a single @keyframes block without performance concerns. The GPU handles it. Where things get expensive is stacking multiple animating clip-paths on the same compositing layer — keep animated clips on their own elements with will-change: clip-path if you're running more than three simultaneously.
Combining clip-path with Other CSS Effects
Here's where things get fun. clip-path stacks well with filter, backdrop-filter, mix-blend-mode, and transforms. The catch is compositing order: clipping happens after painting but before most composite operations, so a blurred element is clipped to the shape, not the other way around.
Want a polygon-shaped glassmorphism card? Use a wrapper element clipped to the polygon and apply backdrop-filter: blur() to the wrapper. Don't try to clip the blurred element directly — you'll lose the blur because the composite layer gets reconstructed:
.polygon-glass-wrapper {
clip-path: polygon(0 0, 100% 0, 100% 80%, 80% 100%, 0 100%);
/* This clips the composited result */
}
.polygon-glass-card {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.2);
/* Full size, inherits parent clip */
}That two-element pattern keeps blur and clip decoupled. Works in every modern browser. If you want the full glassmorphism token set to pair with your custom polygon shapes, the glassmorphism generator will export ready-to-use CSS you can drop straight in.
One more thing — clip-path interacts unexpectedly with overflow: hidden. You don't need both on the same element. clip-path already hides the overflow area, so overflow: hidden just adds a redundant stacking context. Remove it.
Practical Patterns to Ship Right Now
Diagonal hero dividers, angular card cuts, hexagonal avatars, clipped image reveals — these are the four shapes you'll actually reach for in production. Here's each one as a drop-in snippet.
/* 1. Diagonal hero bottom */
.hero {
clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%);
padding-bottom: 64px;
}
/* 2. Beveled card corner (top-right) */
.card-bevel {
clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%);
}
/* 3. Hexagonal avatar */
.avatar-hex {
width: 80px;
height: 80px;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
/* 4. Image reveal on hover */
.img-reveal {
clip-path: inset(0 100% 0 0);
transition: clip-path 0.6s ease;
}
.img-reveal:hover,
.img-reveal.visible {
clip-path: inset(0 0% 0 0);
}All four patterns work without prefixes in 2026. The hexagonal avatar in particular is something you see on gaming dashboards and Discord-style UIs — a clean 80px × 80px element clipped to a six-point polygon with zero SVG markup required.
If you're building a design system and want shape tokens baked in alongside gradient and shadow tokens, the box shadow generator and gradient tools at Empire UI export values that pair directly with these clip shapes. Same visual language, consistent output.
FAQ
The clipped area is invisible but the hit area stays rectangular by default — users can click 'outside' the visible shape. For interactive elements like buttons, add a matching SVG clip region via pointer-events or restructure your DOM so the clickable element matches the visible region.
Tailwind doesn't ship polygon utilities, but you can add them via theme.extend.clipPath in your config, or use inline styles for one-offs. The [clip-path:polygon(...)] arbitrary value syntax works in Tailwind v3+ if you need it in JSX.
Apply clip-path to a wrapper element, not the element with backdrop-filter. Clipping a composited blur layer collapses the blur. Two elements, one clips, one blurs — that pattern solves it every time.
Same syntax, different property. clip-path: path() controls what part of an element is visible. shape-outside: path() controls how inline text flows around a floated element. They look identical in code but serve completely different layout purposes.