Type something to search...
How to Draw Shapes with Pure CSS

How to Draw Shapes with Pure CSS

Drawing shapes with CSS is part party trick, part genuinely useful skill. Tooltips need arrows, badges need circles, ribbons need notches, and loading indicators need rings. You could reach for an SVG every time, but for small, simple shapes, a few lines of CSS are often lighter, easier to theme, and easier to animate.

In this guide we'll build a toolbox of shapes, from the obvious (circles and squares) to the clever (hearts, stars, and speech bubbles), and look at the four techniques that make almost all of them possible: border-radius, border tricks, clip-path, and pseudo-elements. Along the way, I'll point out when a shape belongs in CSS and when you should just use SVG.

The Four Core Techniques

Almost every CSS shape uses one or more of these:

  1. border-radius rounds corners, and at extreme values creates circles, ellipses, pills, and organic blobs.
  2. Border tricks exploit the way borders meet at diagonal seams to create triangles.
  3. clip-path cuts an element into any polygon or curve.
  4. Pseudo-elements (::before and ::after) give you two extra boxes per element to combine into compound shapes.

For every example below, I'll use a single element:

<div class="shape"></div>

With a shared base style:

.shape {
  width: 120px;
  height: 120px;
  background: #38bdf8;
}

Shapes with border-radius

Circle

.circle {
  width: 120px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #38bdf8;
}

aspect-ratio: 1 guarantees a square, and border-radius: 50% rounds each corner by half the width and height, which produces a perfect circle.

Ellipse

.ellipse {
  width: 200px;
  height: 120px;
  border-radius: 50%;
  background: #818cf8;
}

The same 50% on a rectangle gives an ellipse, because percentage radii are calculated separately for the horizontal and vertical axes.

Pill

.pill {
  width: 200px;
  height: 64px;
  border-radius: 999px;
  background: #34d399;
}

A very large length radius gets clamped to half the shortest side, so the ends become perfect semicircles no matter the width. That's why pill-shaped buttons use a huge radius instead of 50%.

Half Circle

.half-circle {
  width: 200px;
  height: 100px;
  border-radius: 100px 100px 0 0;
  background: #fbbf24;
}

Leaf and Egg Shapes

border-radius accepts separate horizontal and vertical radii for each corner, separated by a slash. That unlocks organic shapes:

.leaf {
  width: 120px;
  aspect-ratio: 1;
  border-radius: 0 100% 0 100%;
  background: #34d399;
}

.egg {
  width: 120px;
  height: 160px;
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
  background: #fde68a;
}

The egg uses equal horizontal radii but larger vertical radii at the top, making the top narrower than the bottom.

Blobs

With eight different radius values, you get the soft, irregular "blob" shapes popular in modern illustration:

.blob {
  width: 200px;
  aspect-ratio: 1;
  border-radius: 42% 58% 70% 30% / 45% 45% 55% 55%;
  background: linear-gradient(135deg, #818cf8, #f472b6);
  animation: morph 8s ease-in-out infinite;
}

@keyframes morph {
  50% {
    border-radius: 70% 30% 46% 54% / 30% 39% 61% 70%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .blob {
    animation: none;
  }
}

Because border-radius values interpolate smoothly, animating between two sets of radii makes the blob wobble organically.


Triangles with Borders

The border triangle is the oldest CSS shape trick. When an element has zero width and height, its borders meet at diagonal seams in the middle. Make three of them transparent and the fourth forms a triangle.

.triangle-up {
  width: 0;
  height: 0;
  border-left: 60px solid transparent;
  border-right: 60px solid transparent;
  border-bottom: 100px solid #f472b6;
}
  • The bottom border's height sets the triangle's height (100px).
  • The left and right borders together set its width (120px).
  • The transparent side borders slice off the diagonals.

Swap which border has the color to point the triangle in other directions:

.triangle-right {
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-bottom: 50px solid transparent;
  border-left: 90px solid #fbbf24;
}

.triangle-corner {
  width: 0;
  height: 0;
  border-top: 100px solid #38bdf8;
  border-right: 100px solid transparent;
}

The last one is a right-angle triangle that sits in the top-left corner, useful for "new" ribbons on card corners.

Triangles with clip-path

Border triangles work everywhere, but they can't have gradients, and sizing them is unintuitive. clip-path is often cleaner today:

.triangle-clip {
  width: 120px;
  height: 100px;
  background: linear-gradient(#f472b6, #818cf8);
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

You get normal width and height, and any background you like.


Polygon Shapes with clip-path

clip-path: polygon() handles every straight-edged shape. Here's a collection:

.diamond {
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

.pentagon {
  clip-path: polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%);
}

.hexagon {
  clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}

.octagon {
  clip-path: polygon(
    30% 0,
    70% 0,
    100% 30%,
    100% 70%,
    70% 100%,
    30% 100%,
    0 70%,
    0 30%
  );
}

.star {
  clip-path: polygon(
    50% 0%,
    61% 35%,
    98% 35%,
    68% 57%,
    79% 91%,
    50% 70%,
    21% 91%,
    32% 57%,
    2% 35%,
    39% 35%
  );
}

.chevron-right {
  clip-path: polygon(0 0, 75% 0, 100% 50%, 75% 100%, 0 100%, 25% 50%);
}

.parallelogram {
  clip-path: polygon(25% 0, 100% 0, 75% 100%, 0 100%);
}

Apply any of them to the base .shape element. The five-point star alternates between outer points and inner corners, ten vertices in total.

A handy alternative for parallelograms is transform: skewX(), which keeps the element's content editable and lets you un-skew the text inside:

.tag-skew {
  display: inline-block;
  padding: 0.4rem 1.2rem;
  background: #818cf8;
  transform: skewX(-15deg);
}

.tag-skew > span {
  display: inline-block;
  transform: skewX(15deg);
}

Compound Shapes with Pseudo-Elements

When one box isn't enough, ::before and ::after give you two more. Position them relative to the main element and you can build shapes out of multiple parts.

Heart

A heart is two rounded rectangles rotated in opposite directions:

.heart {
  position: relative;
  width: 100px;
  height: 90px;
}

.heart::before,
.heart::after {
  content: "";
  position: absolute;
  top: 0;
  width: 50px;
  height: 80px;
  border-radius: 50px 50px 0 0;
  background: #f472b6;
}

.heart::before {
  left: 50px;
  transform: rotate(-45deg);
  transform-origin: 0 100%;
}

.heart::after {
  left: 0;
  transform: rotate(45deg);
  transform-origin: 100% 100%;
}

Each pseudo-element is a tall shape with a rounded top. Rotating one left and one right from their bottom corners makes them meet in a point at the bottom, forming the heart.

A simpler modern alternative uses a rotated square plus two circles made from gradients, but the pseudo-element version is a great way to learn how compound shapes are assembled.

Speech Bubble

A rounded rectangle with a triangle tail:

<p class="bubble">Great work on the launch!</p>
.bubble {
  position: relative;
  max-width: 18rem;
  margin: 0 0 1.5rem;
  padding: 1rem 1.25rem;
  border-radius: 16px;
  background: #1f2937;
  color: #e2e8f0;
}

.bubble::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 1.5rem;
  border-width: 12px 12px 0 0;
  border-style: solid;
  border-color: #1f2937 transparent transparent transparent;
}

The tail is a border triangle positioned just below the bubble. Since it uses the same color as the bubble, they read as one shape.

Crescent Moon

A crescent is just a circle with an offset shadow:

.moon {
  width: 120px;
  aspect-ratio: 1;
  border-radius: 50%;
  box-shadow: 24px 12px 0 0 #fbbf24;
}

The element itself is transparent. Only the shadow is visible, and since it's offset, the shape looks like a crescent.

Ring and Donut

.ring {
  width: 120px;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 16px solid #38bdf8;
}

Turn it into a loading spinner by coloring one side differently and rotating it:

.spinner {
  width: 48px;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 5px solid #334155;
  border-top-color: #38bdf8;
  animation: spin 0.9s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}

Cross or Plus

.plus {
  width: 100px;
  aspect-ratio: 1;
  background:
    linear-gradient(#34d399, #34d399) center / 100% 24px no-repeat,
    linear-gradient(#34d399, #34d399) center / 24px 100% no-repeat;
}

This one uses no pseudo-elements at all, just two gradient layers sized into bars. Gradients are a secret weapon for shapes: any rectangle, circle, or stripe can be painted as a background layer.


Shapes with Gradients

Beyond the plus sign, gradients can draw many shapes directly into the background:

.dot-grid {
  width: 200px;
  height: 120px;
  background: radial-gradient(circle, #818cf8 6px, transparent 7px) 0 0 / 24px
    24px;
}

.pie-slice {
  width: 120px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(#fbbf24 0 90deg, transparent 0);
}

The dot grid tiles a small circle, and the pie slice uses a conic gradient with a hard stop. Neither needs any extra markup.


Making Shapes Accessible

Shapes are usually decorative, but when they carry meaning, give them an accessible name:

<span
  class="status-dot status-dot--online"
  role="img"
  aria-label="Online"
></span>
.status-dot {
  display: inline-block;
  width: 0.75rem;
  aspect-ratio: 1;
  border-radius: 50%;
}

.status-dot--online {
  background: #34d399;
}

If the shape is purely decorative, keep it out of the accessibility tree with aria-hidden="true", or build it with pseudo-elements, which assistive technologies generally ignore when they have empty content.

Also remember that color alone shouldn't convey meaning. A green dot for "online" should be paired with text or a different shape for other states.

When to Use SVG Instead

CSS shapes are great for simple, geometric UI pieces. Reach for SVG when:

  • The shape is complex or illustrative, like a logo or a detailed icon.
  • You need precise curves that border-radius and polygon() can't express.
  • The shape needs strokes that follow curved edges exactly.
  • You want to reuse the same shape across many places with a sprite or symbol.

A useful test: if you're stacking more than two pseudo-elements and a handful of transforms, SVG is probably simpler and more maintainable.

Best Practices

  • Prefer aspect-ratio over fixed width and height pairs, so shapes scale from a single dimension.
  • Use custom properties for color and size, so a shape can be themed from one place:
.badge-shape {
  --size: 3rem;
  --color: #818cf8;
  width: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--color);
}
  • Prefer clip-path over border triangles when you need gradients or intuitive sizing, and keep border triangles for tiny tooltip arrows.
  • Remember what clip-path hides. Shadows and outlines get clipped, so use filter: drop-shadow() on a parent if you need a shadow.
  • Test at different sizes. Shapes built with pixel values may break when resized. Percentages and em units scale more gracefully.

Conclusion

With border-radius, border tricks, clip-path, pseudo-elements, and gradients, you can draw most of the simple shapes an interface needs without a single image file. Circles, pills, and blobs come from border-radius. Triangles come from borders or polygons. Stars, hexagons, and arrows come from clip-path. Hearts, speech bubbles, and moons come from combining boxes and shadows.

Use these techniques for small, themeable, animatable UI details, and switch to SVG when shapes grow complex. Knowing both gives you the right tool for every job, and a fun way to sharpen your CSS skills along the way.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading