A Modern Approach to CSS Shapes
Searching for “how to create [shape] with CSS” is practically a rite of passage for front-end developers. The results are endless, and it’s tempting to copy the first snippet you find, drop it into your stylesheet, and move on. The problem? Many of those snippets are outdated, rely on magic numbers, and are hard to modify when your design inevitably changes.
This guide covers the most common CSS shapes using clean, modern techniques. The focus is not on memorizing a specific shape’s code, but on understanding the reusable tricks that let you build any shape you need with minimal, flexible markup.
CSS or SVG?
It’s a fair question: why not just use SVG for shapes? The honest answer is that you should use SVG if that’s the better tool for your project. SVG is a perfectly valid approach with its own syntax and considerations.
CSS, however, is often the better choice for decorative elements or when you need to style a specific HTML element that already contains real content. There’s no universal winner here. Your project’s requirements should guide whether a CSS shape fits the bill or whether SVG is the more appropriate solution.
A Handy Reference
Before diving into the code, it’s worth spending a few minutes exploring the CSS Shape website. It’s a growing collection of CSS-only shapes that I maintain regularly. Each shape’s CSS is optimized to be as flexible and efficient as possible. In most cases, the styles target a single HTML element, so you don’t have to clutter your markup. CSS variables are used liberally throughout, making it easy to tweak a shape to fit your needs without rewriting the underlying logic.
If you don’t have time to learn every technique by heart, that resource is a practical, ready-to-use reference you can bookmark for future projects.
Clipping Shapes With clip-path
The clip-path property with its polygon() function is the standard tool for CSS shapes. By working through several classic shapes, a few useful tricks emerge that make broader shape creation easier to handle.
Hexagons Without Full Coordinates
Creating a hexagon by listing all six of its points is straightforward: define dimensions and provide coordinates for each point.
.hexagon {
width: 200px;
aspect-ratio: 0.866;
clip-path: polygon(
0% 25%,
0% 75%,
50% 100%,
100% 75%,
100% 25%,
50% 0%);
}
See the Pen [Hexagon shape using clip-path](https://codepen.io/t_afif/pen/JjVJJbG) by Temani Afif.
But there is a simpler route using just four points. The polygon() function accepts coordinates outside the element’s range — beyond [0% 100%]. You can clip outside the element’s boundaries, which cuts down the points needed.
This draws a diamond shape where two points extend well past the intended boundaries. The lesson: allow yourself to think beyond the shape’s perimeter. The resulting CSS is noticeably compact:
.hexagon {
width: 200px;
aspect-ratio: cos(30deg);
clip-path: polygon(
-50% 50%,
50% 100%,
150% 50%,
50% 0
);
}
Note the replacement of the magic number 0.866 with the trigonometric function cos(). The aspect ratio equals cos(30deg) in this construction, which is easier to recall than a hardcoded decimal.
Swapping the X and Y coordinate values produces a variation of the hexagon — a rotation that changes its orientation.
clip-path: polygon(X1 Y1, X2 Y2, ..., Xn Yn)
clip-path: polygon(Y1 X1, Y2 X2, ..., Yn Xn)
The result is a different arrangement of the same fundamental shape:
See the Pen [Another variation of the hexagon shape](https://codepen.io/t_afif/pen/BaEZrrP) by Temani Afif.
The aspect ratio flips to 1/cos(30deg) when axes are swapped. Since the coordinates change places, the ratio must invert to maintain the correct proportions.
Because this is a single style rule on one selector, it applies equally to an <img> element as a <div>:
See the Pen [CSS-only hexagon shapes (the modern way)](https://codepen.io/t_afif/pen/KKEMjxV) by Temani Afif.
Two practical lessons emerge here:
- The
polygon()function tolerates points outside the[0% 100%]range. This allows fewer clipping points and creates possibilities for additional shapes. - Swapping axes generates shape variations. In the hexagon above, trading X and Y values changes the hexagon’s direction.
Octagons and Repeated Values
An octagon has eight sides — think of a traffic stop sign. Applying the same trick from the hexagon, those eight sides can be established with only four points by using coordinates that fall outside the element’s bounds.
While visualizing outside points takes some adjustment, the CSS looks familiar:
.octagon {
width: 200px;
aspect-ratio: 1;
--o: calc(50% * tan(-22.5deg));
clip-path: polygon(
var(--o) 50%,
50% var(--o),
calc(100% - var(--o)) 50%,
50% calc(100% - var(--o))
);
}
Aside from a small trigonometric formula, the structure matches the hexagon: set dimensions, clip the points. The calculation is stored as a CSS variable to avoid repetition. For those who prefer not to derive such formulas themselves, references exist — an online collection of CSS shapes serves as a good starting point.
This shape applies to images just as easily:
See the Pen [CSS-only octagon shapes (the modern way)](https://codepen.io/t_afif/pen/LYaxqEg) by Temani Afif.
There is still room to simplify the code further:
.octa {
--w: 200px;
width: var(--w);\
aspect-ratio: 1;
margin: calc(var(--w) * tan(22.5deg) / 2);
clip-path: polygon(0 50%, 50% 0, 100% 50%, 50% 100%) margin-box;
}
See the Pen [Octagon shape with margin-box](https://codepen.io/t_afif/pen/ZEZrLmr) by Temani Afif.
The variable holding the math value (--o) is gone, replaced by --w for dimensions. A broader change: the margin property is set, and clip-path references the margin-box keyword. Instead of the default border-box reference, the clipping is now relative to the margin box.
Looking at the octagon’s four points, they sit outside the shape’s boundaries at an equal distance. Rather than incorporating that distance into each coordinate, declaring it on margin makes coordinate values much easier to derive.
From the original:
.octagon {
--o: calc(50% * tan(-22.5deg));
clip-path: polygon(var(--o) 50%, 50% var(--o), calc(100% - var(--o)) 50%, 50% calc(100% - var(--o)));
}
The revised version has a clearer clip-path, with simplicity traded for an extra property:
.octagon {
--w: 200px;
margin: calc(var(--w) * tan(22.5deg) / 2);
clip-path: polygon(0 50%, 50% 0, 100% 50%, 50% 100%) margin-box;
}
All --o variables vanish from clip-path, and the margin absorbs that value. A new variable --w defines the element’s dimensions because percentage values no longer work directly. This results in some margin around the element, but the calculation becomes simpler. To avoid the extra margin, use padding paired with negative margin of the same amount — another technique for keeping polygon() simple with images:
See the Pen [Different shapes using the same polygon](https://codepen.io/t_afif/pen/oNOOWqz) by Temani Afif.
Five Points for a Star
Stars are tricky with clip-path because the coordinates need precision. A typical approach uses ten points, one for each vertex of the star’s outline.
But five points suffice, provided that the order of coordinates inside polygon() is handled deliberately. If you were drawing a star on paper without lifting the pen, you would trace the outline in a particular sequence:
The same continuous-line logic transfers cleanly to CSS. The lines inside the clipping path can intersect — the classic star pattern relies on it. That insight often gets overlooked when building shapes by coordinates:
.star {
width: 200px;
aspect-ratio: 1;
clip-path: polygon(50% 0, /* (1) */
calc(50%*(1 + sin(.4turn))) calc(50%*(1 - cos(.4turn))), /* (2) */
calc(50%*(1 - sin(.2turn))) calc(50%*(1 - cos(.2turn))), /* (3) */
calc(50%*(1 + sin(.2turn))) calc(50%*(1 - cos(.2turn))), /* (4) */
calc(50%*(1 - sin(.4turn))) calc(50%*(1 - cos(.4turn))) /* (5) */
);
}
See the Pen [Star shape using clip-path](https://codepen.io/t_afif/pen/NWmvBeL) by Temani Afif.
The trigonometric calculations keep accuracy high without magic numbers, the five-point implementation beats the ten-point equivalent:
.star {
width: 200px;
aspect-ratio: 1;
clip-path: polygon(50% 0, 79% 90%, 2% 35%, 98% 35%, 21% 90%);
}
Looking at the coordinates, the star’s symmetry matters. The second and fifth points share Y coordinates; so do the third and fourth. The X values sit equidistant from the center: 79% - 50% = 50% - 21% equals 100% when summed.
This yields another lesson: look for symmetry to spot duplicated values. With patterns that mirror themselves, only three points truly need to be remembered or derived; the remaining ones come from the symmetry.
50% 0 /* (1) */
79% 90% /* (2) --> (100% - 79%) = 21% 90% /* (5) */
2% 35% /* (3) --> (100% - 2%) = 98% 35% /* (4) */
Re-examining the hexagon and octagon with this lens also reveals repeated values that make the clip-path rules easier to memorize.
Polygons, Starbursts, and Slanted Rectangles
For cases where the number of points is unknown or configurable, shapes such as regular polygons and starbursts provide flexible generics. Starbursts are simply polygons where half of the interface points are moved inward toward the center:

The coordinates can get tedious by hand, and online generators offer a better path for these dynamic shapes:
Still, understanding how the coordinates get calculated is worthwhile — an article on computing polygon and starburst coordinates covers those details.
For rectangles with one or two angled sides — parallelograms, trapezoids, skewed rectangles — the same CSS technique applies across all of them.
Start with the basic rectangle described by its four corner points:
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%)
This code renders nothing new because the element is already a rectangle, and the values are just 0 and 100%.
Then offset some values until the shape changes as wanted. With a 10px offset, a 0 becomes 10px, and a 100% becomes calc(100% - 10px).
Which value needs updating and when?
That answer is best found experimentally. Opening the browser’s developer tools and adjusting values live shows exactly how each coordinate moves the shape. Even experienced CSS developers rarely write complete shapes from memory without verifying visually. Starting from the basic rectangle and adding or updating points until arriving at the desired shape is common practice. For reference shapes, an online collection contains the full working code along with additional shapes to try.
Cutting Shapes With CSS Masks
Where clip-path handles polygon-based shapes, the mask property opens up circular and curvy possibilities that require gradients rather than coordinate plotting. The core concept is simple: gradients define which parts of an element remain visible. Everything transparent in the mask layer gets cut away.
Creating Holes and Circular Cuts
A radial-gradient makes quick work of punching a circular hole through an element:
mask: radial-gradient(50px, #0000 98%, #000);
Why reach for mask over a plain background? Masks offer more flexibility — any color works, and the technique applies to elements like <img> just as easily:
See the Pen [Hole shape](https://codepen.io/t_afif/pen/OJGgGve) by Temani Afif.
Here is the first important lesson: the colors themselves don't matter when working with mask. Only the alpha channel counts. Opaque colors (commonly #000) keep elements visible; fully transparent colors (like #0000) cut them out.
Use hard color stops to achieve crisp edges, though leaving a tiny transition — say, stopping at 98% instead of 100% — helps avoid jagged rendering.
Shifting gradient positions produces different cuts. A circle sliced from an element's edge:
See the Pen [Circular cut from the top & bottom](https://codepen.io/t_afif/pen/MWRvBOL) by Temani Afif.
The same principle extends to cutting from both top and bottom:
See the Pen [Circular Cut at top and bottom](https://codepen.io/t_afif/pen/WNWEKdy) by Temani Afif.
Give the radial-gradient an explicit size so it repeats, and the cut becomes a decorative scooped border:
See the Pen [Scooped edges from top and bottom](https://codepen.io/t_afif/pen/eYoEjVa) by Temani Afif.
That last example shows how a few configuration tweaks pivot from a simple hole to an ornamental edge. Fancy borders of all kinds — wavy, spiked, scalloped — follow this same mask-and-gradient recipe. Two useful references for expanding on the technique:
- "Fancy CSS Borders Using Masks" (CSS-Tricks)
- "How to Create Wavy Shapes & Patterns in CSS" (CSS-Tricks)
That second article also shows the technique applied to decorative background patterns.
See the Pen [CSS only pattern](https://codepen.io/t_afif/pen/vYddpzK) by Temani Afif.
Building Rounded Arcs With Composition
Gradients remain the tool of choice for rounded arc shapes — the kind frequently seen in loading spinners:
This example introduces composition, an operation between two or more gradient mask layers. Composition can be defined with mask-composite or declared directly on the mask property. The diagram below shows how the layers stack:
The pattern: a radial-gradient makes a full circle, a conic-gradient adds another shape below it, and an intersect composition between them leaves an unclosed circle. Two extra radial gradients add the rounded endpoints using the default add composition.
Gradients are familiar territory from styling background, but composition is the new trick worth keeping in mind — it unlocks a wide range of forms.
.arc {
--b: 40px; /* border thickness */
--a: 240deg; /* progression */
--_g:/var(--b) var(--b) radial-gradient(50% 50%,#000 98%,#0000) no-repeat;
mask:
top var(--_g),
calc(50% + 50% * sin(var(--a)))
calc(50% - 50% * cos(var(--a))) var(--_g),
conic-gradient(#000 var(--a), #0000 0) intersect,
radial-gradient(50% 50%, #0000 calc(100% - var(--b)), #000 0 98%, #0000)
}
See the Pen [Progress circle using mask](https://codepen.io/t_afif/pen/eYoEpom) by Temani Afif.
Even when the syntax looks intimidating, CSS variables make adjustments straightforward. Most shapes benefit from configuring a few variables that drive the underlying math. Don't get lost in the formulas; focus on how gradients and composition combine to achieve the effect.
Notably, the same visual result can be reached with entirely different gradient setups:
.arc {
--b: 40px; /* border thickness */
--a: 250deg; /* progression */
padding: var(--b);
border-radius: 50%;
--_g: /var(--b) var(--b) radial-gradient(50% 50%, #000 97%, #0000 99%) no-repeat;
mask:
top var(--_g),
calc(50% + 50% * sin(var(--a)))
calc(50% - 50% * cos(var(--a))) var(--_g),
linear-gradient(#0000 0 0) content-box intersect,
conic-gradient(#000 var(--a), #0000 0);
}
This variant applies border-radius to round the element and adds padding equal to the border thickness. The mask's radial-gradient is swapped for a linear-gradient with a single transparent color covering the content-box.
That approach adds two more variables but simplifies the gradients themselves — another valid way to reach the same shape.
See the Pen [Untitled](https://codepen.io/t_afif/pen/WNWErpV) by Temani Afif.
Dashed Circles and Rounded Tabs
Dashed circular edges reuse nearly identical code:
See the Pen [Dashed border](https://codepen.io/t_afif/pen/KKvjjZN) by Temani Afif.
Two gradients do the work here: a black-to-transparent repeating-conic-gradient provides the dash pattern, while a transparent linear-gradient restricts visibility to the content-box. An intersect composition combines them.
mask:
linear-gradient(#0000 0 0) content-box intersect,
repeating-conic-gradient( /* ... */ );
For a deeper dive into how mask-composite behaves, Ana Tudor's "Mask Compositing: The Crash Course" is a solid resource.
Tabs present a more structured challenge. The rounded top corners are straightforward, but the inward-curving bottom edges require careful masking:
Positioning a pseudo-element behind the panel group is one approach, though it introduces fixed values and extra complexity. Masks keep the solution minimal and reusable.
The process begins by adding borders — excluding the bottom — with border-radius on the top corners:
.tab {
--r: 40px; /* radius size */
border: var(--r) solid #0000; /* transparent black */
border-bottom: 0;
border-radius: calc(2 * var(--r)) calc(2 * var(--r)) 0 0;
}
The first mask layer displays only the padding area:
mask: linear-gradient(#000 0 0) padding-box;
Two radial gradients then reveal the bottom curves:
mask:
radial-gradient(100% 100% at 0 0, #0000 98%, #000) 0 100% / var(--r) var(--r),
radial-gradient(100% 100% at 100% 0, #0000 98%, #000) 100% 100% / var(--r) var(--r),
linear-gradient(#000 0 0) padding-box;
Full implementation:
.tab {
--r: 40px; /* control the radius */
border: var(--r) solid #0000;
border-bottom: 0;
border-radius: calc(2 * var(--r)) calc(2 * var(--r)) 0 0;
mask:
radial-gradient(100% 100% at 0 0, #0000 98%, #000) 0 100% / var(--r) var(--r),
radial-gradient(100% 100% at 100% 0, #0000 98%, #000) 100% 100% / var(--r) var(--r),
linear-gradient(#000 0 0) padding-box;
mask-repeat: no-repeat;
background: linear-gradient(60deg, #BD5532, #601848) border-box;
}
Notice the border-radius value in the complete setup:
border-radius: calc(2 * var(--r)) calc(2 * var(--r)) 0 0;
The rounded top edges equal two times the --r variable. Doubling is necessary because of the transparent border; the blue highlighted regions span 2 * R, while the red region measures 2 * R - R, or simply R.
An optimized version drops to two gradients — one linear, one radial — rather than three. See if you can determine how one gradient was eliminated.
See the Pen [Rounded tab using CSS mask](https://codepen.io/t_afif/pen/JjVpPmr) by Temani Afif.
Knowing when code can be tightened is genuinely difficult. There is no formula for spotting optimizations. Start with the most obvious solution, even if it uses many gradients, then refine with practice.
For practice, try applying the same masking logic to tooltip shapes, which position inward curves on the left, right, or both sides:
Reference implementations are available in the online collection.
Shape Recipes: From Triangles to Floral Patterns
The techniques covered so far — gradients and mask for curves, clip-path for sharp edges — cover most of what you need. Applying them to specific shapes is often just a matter of configuration. Here is a rundown of common shapes, along with the formulas and resources that make them easy to reproduce.
Triangles
Triangles appear everywhere in UI work: play buttons, link arrows, accordion toggles. A basic triangle is a three-point polygon with dimensions set on the element:
.triangle {
width: 200px;
aspect-ratio: 1;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
Adding more polygon points produces border-only variants:
See the Pen [border-only triangle shapes](https://codepen.io/t_afif/pen/XWGzJpP) by Temani Afif.
clip-path with mask:
See the Pen [Rounded triangles (the modern way)](https://codepen.io/t_afif/pen/QWovwoW) by Temani Afif.
Hearts
Hearts have traditionally required awkward hacks, but modern CSS reduces that to combining border-image with clip-path:
.heart {
--c: red;
width: 200px;
aspect-ratio: 1;
border-image: radial-gradient(var(--c) 69%,#0000 70%) 84.5%/50%;
clip-path: polygon(-42% 0,50% 91%, 142% 0);
}
See the Pen [Heart shape using border-image](https://codepen.io/t_afif/pen/MWPOJpP) by Temani Afif.
mask-border to apply heart shapes directly to images:
See the Pen [CSS only heart images](https://codepen.io/t_afif/pen/PoRwjPM) by Temani Afif.
Ribbons
Ribbons are less of a single shape and more of a family — one with well over 100 variations in a dedicated collection. Rather than present one pattern, four articles cover the core mechanics and the range of possible designs:
- CSS Responsive Multi-Line Ribbon Shapes, Part 1 (Smashing Magazine)
- CSS Responsive Multi-Line Ribbon Shapes, Part 2 (Smashing Magazine)
- CSS Shapes: The Ribbon (Verpex Blog)
- How to Create CSS Ribbon Shapes with a Single Element (SitePoint)
Tooltips and Speech Bubbles
Tooltips and speech bubbles share the same open-ended design space — again, more than 100 possibilities exist in a dedicated collection. The essential patterns are thoroughly covered in:
- Modern CSS Tooltips And Speech Bubbles, Part 1 (Smashing Magazine)
- Modern CSS Tooltips And Speech Bubbles, Part 2 (Smashing Magazine)
Master those, and you can generate as many variations as you can dream up.
Cutting Corners and Cut-Outs
Removing corners from squares and rectangles creates attractive frames, useful either as decoration or as a backdrop for images. You can cut all corners or just some; the cuts can be sharp or rounded, and the overall shape can have an outline.
A related idea is cutting an entire shape out of a rectangle — so-called inverted shapes. This is nothing new; it is just a clip-path with coordinates set in a polygon(). You already have that skill from the earlier examples in this guide:
See the Pen ["Cut-out shapes using clip-path"](https://codepen.io/t_afif/pen/gOJvdav) by Temani Afif ([@t_afif](https://codepen.io/t_afif))
Section Dividers and Inner Curves
When you want visual transitions between page sections, one option is pair decorative borders that lock together:
The unifying pattern should by now be clear: whether you are clipping an element, masking it, or carving into it with gradients and coordinate points, the result is far more flexible than the workarounds of earlier CSS.
A full walkthrough of divider techniques can be found in “How to Create a Section Divider Using CSS” (freeCodeCamp).
Inner-curve shapes — known variously as inverted radius, notch, or bell curves — also rely on combining gradients within a mask to form variations:
Reference “How to create Shapes with Inner Curves using CSS Mask” (Verpex Blog) for the underlying logic, then see the technique in action:
See the Pen ["Inverted border-radius using CSS mask"](https://codepen.io/t_afif/pen/XWLJrWE) by Temani Afif ([@t_afif](https://codepen.io/t_afif))
See the Pen ["Fancy avatar header with hover effect"](https://codepen.io/t_afif/pen/oNrMJXL) by Temani Afif ([@t_afif](https://codepen.io/t_afif))
Floral Shapes
Combine circles and waves and you get floral shapes:
These do duty as decoration on their own but really shine when applied to images. Masking the edges of a photograph transforms it into a fancy custom frame. For instance, they power a pop-out hover effect in this demo:
See the Pen [Fancy Pop Out hover effect!](https://codepen.io/t_afif/pen/qBQzrwq) by Temani Afif.
Creating them involves trigonometric functions. If you want to understand the mathematics, two articles have all the details:
- Creating Flower Shapes using CSS Mask & Trigonometric Functions (Frontend Masters)
- Creating Wavy Circles with Fancy Animations in CSS (Frontend Masters)
Wavy and Zig-Zag Boxes
Earlier examples put a wave or zig-zag on a single side. There is no need to stop there — the techniques extend to all four sides, creating full ornamented containers:
The recipe behind each is easy to parse with guidance:
- How to Create a Zig-Zag Box Using CSS (Verpex Blog)
- How to Create Wavy Boxes Using CSS (Verpex Blog)
A few demos show these boxes decorating images:
See the Pen ["Images inside wiggly boxes"](https://codepen.io/t_afif/pen/gbYBPma) by Temani Afif ([@t_afif](https://codepen.io/t_afif))
See the Pen Images inside wavy boxes by Temani Afif (@t_afif) on CodePen.
See the Pen CSS-only Zig-Zag box by Temani Afif (@t_afif) on CodePen.
Looking Back, Looking Forward
The set of shapes covered here is really a combinatorial system. Clipping, masking, gradients, composition, CSS variables — plus a few hidden features — account for hundreds of outcomes. In particular, three facts about polygon() unlock most of this:
- It accepts coordinates outside the
[0% 100%]range, which makes possible clipping paths that extend beyond the element box. - Swapping axes is a fast way to generate new variations of a shape.
- Line segments in the polygon can intersect, opening up unexpected silhouettes.
No need to memorize snippets; understanding these mechanics is what lets you adapt the examples into any context. For quick reference and immediate source code, the CSS Shape website has the math already worked out for many shapes — and doubles as a springboard for inventing your own.
If you have a shape in mind that is not in the collection, consider adding it; the underlying logic makes most designs possible.
Reference Material
- CSS Shapes: Polygon & Starburst (Verpex Blog)
- CSS Tricks To Master The
clip-pathProperty (Verpex Blog) - Fancy CSS Borders Using Masks (CSS-Tricks)
- How to Create Wavy Shapes & Patterns in CSS (CSS-Tricks)
- Mask Compositing: The Crash Course (Ana Tudor, CSS-Tricks)




