Basic Shapes & Path… Never the Twain Shall Meet?

Avatar of Chris Coyier
Chris Coyier on

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

There are some values available in CSS that allow shapes to be drawn. For example, there is a circle() function that is a valid value for a couple of CSS properties. “Drawn” might not be the right word, though. It’s not like in SVG where you can create a <circle> element and it will literally draw a circle.

These shapes in CSS are for other things. Namely: clip-path, which is for making clipping masks, and shape-outside, for flowing text around shapes.

There are some other CSS properties that use SVG-like shapes for doing what they do. For example, offset-path is part of animating elements along vector paths, using the path() function. Paths are awesome. They are the ultimate drawing element, as they can draw anything, and all the other shapes are essentially syntactic sugar for paths.

What are the basic shapes?

There are four basic shapes:

  • polygon()
  • circle()
  • ellipse()
  • inset()

What are paths?

Paths come from SVG. They have a special syntax that allows them to draw anything.

<path d="M 25,100 C 25,150 75,150 75,100 S 100,25 150,75" />
path("M 25,100 C 25,150 75,150 75,100 S 100,25 150,75");

This is where things get kinda weird

  • The clip-path and shape-outside properties can take all the “basic shapes” like circle() and polygon(), but not path().
  • The offset-path properties can take path(), but not the “basic shapes”.
  • You can control the d attribute of a <path> through CSS also, but not the attributes of many other SVG elements.

I’m not really sure why any of this is, and I’m sure things will change in time, but it’s good to know about.

Let’s elaborate.

clip-path allows Basic Shapes (but not path())

Say you wanted to clip square avatars into polygons, for a fun design. You can! The image is might be an HTML element like:

<img src="avatar.jpg" alt="User Avatar" class="avatar">
.avatar {
  clip-path: polygon(0% 5%, 100% 0%, 100% 85%, 65% 80%, 75% 100%, 40% 80%, 0% 75%);
}

See the Pen Clipped Avatars by Chris Coyier (@chriscoyier) on CodePen.

But let’s say you want to clip the avatar like this:

Well, you can’t. Not directly in CSS anyway. Those curves require a path, and clip-path don’t do path. Fortunatly, clip-path does take a url(), which will accept the ID of a <clipPath> element.

See the Pen Clipped Avatars by Chris Coyier (@chriscoyier) on CodePen.

offset-path allows path (but not Basic Shapes)

Say we want to move an object along the outside of that speech bubble shape we just used. The offset-path property is just for that.

<div class="move-me"></div>
.move-me {
  offset-path: path("M100.5,39.47C100.5,58.3,83.36,74,60.58,77.64l16.85,19.9L33.94,76.25C14.47,70.92.5,56.47.5,39.47c0-21.52,22.39-39,50-39S100.5,17.95,100.5,39.47Z");
  animation: move 3s linear infinite;
}
@keyframes move {
  100% { motion-offset: 100%;}
}

See the Pen offset-path on path by Chris Coyier (@chriscoyier) on CodePen.

But what if you want to move the element in a circle? The basic shape version of a circle has a super easy syntax, like circle(50% at 50% 50%); But unfortunately, the basic shapes aren’t supported. The spec allows for them, but they don’t work. It kinda makes sense, because… how would you define which direction to travel?

You can still animate in a circle, because the all-powerful path can draw a circle, like:

.move-me {
  /* a circle */
  offset-path: path("M100,50a50,50,0,1,1-50-50A50,50,0,0,1,100,50Z");
  animation: move 3s linear infinite;
}
@keyframes move {
  100% { motion-offset: 100%;}
}

And those commands dictate a direction naturally. There are also other ways to animate in a circle.

shape-outside allows Basic Shapes (but not path())

Say you wanted to wrap some text around an egg shape, because I dunno, you were setting some text of Alice talking to Humpty Dumpty. The egg shape is a good excuse to use the ellipse() Basic Shape.

<div class="page-wrap">
  <div class="egg"></div>
  <p>"I don't know what you mean by 'glory,'" Alice said.</p>
  <p>Humpty Dumpty smiled contemptuously. "Of course you don't—till I tell you. I meant 'there's a nice knock-down argument for you!'"</p>

 ...
.egg {
  float: left;
  shape-outside: ellipse(120px 160px at 50% 50%);
  width: 280px;
  height: 320px;
}

We’d probably set an identical clip-path (to actually make the egg shape) and colorize it:

See the Pen Shape Outside Egg by Chris Coyier (@chriscoyier) on CodePen.

But what if you wanted to make some text wrap around a curved shape, like shown here in Illustrator:

Text Wrap in Adobe Illustrator

Unfortunately, shape-outside doesn’t take path(), so you can’t. But you kinda can. It does take url(), in which you can use to link to an image (doesn’t even have to be SVG, but SVG makes good sense). The image can have a nice curvy path, like we’re shooting for:

See the Pen Wrap Text Around Curve by Chris Coyier (@chriscoyier) on CodePen.

The url() can even be a data URL. Also note that any element using shape-outside must be floated, a limitation that might be resolved with CSS Exclusions.

<path> takes path()

Here’s one that starts out making logical sense. Say you have a path:

<svg>
 <path d=" ... " />
</svg>

You can change the shape of that path through CSS, say through a hover:

svg:hover path {
  d: path(" ... ");
}

See the Pen Change path on hover by Chris Coyier (@chriscoyier) on CodePen.

You can even transition the shape, if it happens to be path data with the same amount of points.

It does get a bit confusing though. Say you have a <polygon> instead.

<svg>
 <polygon points=" ... " />
</svg>

How do you update it with CSS?

polygon {
  /* Nope */
  points: " ... ";

  /* Nope */
  points: points(" ... ");

  /* Nope */
  points: polygon(" ... ");
}

There isn’t a way, that I know of. Which seems weird as polygon is otherwise supported all over the place. I imagine part of problem is that the polygon() function is different from the points attribute. The polygon() function takes percentages and numbers with units in CSS, whereas the points attribute takes unitless numbers (like everything in SVG). They are different beasts, and that overlap is awkward.

It’s also not that SVG shapes that overlap with Basic Shapes can’t be altered. All of <circle>‘s attributes, for example, can be altered with CSS:

svg:hover circle {
  cx: 40;
  cy: 40;
  r: 40;
}

See the Pen Altering Circle Attributes by Chris Coyier (@chriscoyier) on CodePen.

Long story short: there is usually a way to get done what you want to get done, but it’s confusing what is/isn’t supported where.