Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 19 Jun 2025 01:39:31 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Drawing CSS Shapes using corner-shape https://frontendmasters.com/blog/drawing-css-shapes-using-corner-shape/ https://frontendmasters.com/blog/drawing-css-shapes-using-corner-shape/#comments Wed, 18 Jun 2025 17:05:26 +0000 https://frontendmasters.com/blog/?p=6235 We recently got the new shape() function for clip-path which is a game changer for creating CSS shape. Another cool feature is on the way and will soon be available: corner-shape.

I will not call it “new” because it’s something that has been around for quite a while and you can find countless GitHub discussions around it. There is even an 11-year old Github Repo made by Lea Verou with an interactive demo showing a few examples! After all that time, It finally has its own specification and is ready to be implemented and shipped.

At the time of writing, corner-shape is available in Chrome v139 or 136+ with the experimental web features flag turned on, but no other browsers yet.

What is corner-shape?

When you define a border-radius you will get rounded corners. corner-shape allows you to change those rounded corners to something else. It’s in the name; it changes the “shape” of the “corner”.

A graphic displaying different CSS corner shapes: 'round', 'scoop', 'bevel', 'notch', and 'squircle', each in a purple background with white text.

The value round is the default (the classic rounded corners). As you can see above, we have many cool variations. All of this with a simple syntax:

.corner {
  border-radius: 30px;
  corner-shape: round | scoop | bevel | notch | squircle;
}

We can also use the superellipse(K) value that can define all the different variations and more by adjusting the K variable. I will not detail that part as it’s not important for the article but it’s good to know so I invite you to check the (draft) specification for more detail.

Diagram illustrating various 'superellipse()' values for corner shapes with labeled corners and arrows indicating direction. Shows how the corner shape changes with different K values.

Let’s Draw Shapes

Changing the corner shape is good — but how can we draw shapes? The answer is to play with border-radius. Follow along to see all the magic we can do with corner-shape!

Rhombus & Octagon

If you look closely at the example using the bevel value, you will see that we have 8 sides since the corners are diagonal straight lines so we almost have an octagon shape. We simply need to find the exact value for border-radius that gives us 8 equal sides.

I will skip the boring math and give you the final value which is:

border-radius: calc(100%/(2 + sqrt(2)));

Even without being precise, you can approximate the value using trial & error. You will get an octagon when you are close to 29%. The usage of percentage is important because it means the shape is responsive and let’s not forget aspect-ratio: 1 as well.

.octagon {
  border-radius: calc(100%/(2 + sqrt(2)));
  corner-shape: bevel;
  aspect-ratio: 1;
} 

Now what if we keep increasing the radius? We get a Rhombus shape at 50%.

.rhombus {
  border-radius: 50%;
  corner-shape: bevel;
  aspect-ratio: 1;
} 

Some of you might ask if this method is better than what we already have. In my shape collection, you can easily find the code of the above shapes made using clip-path so why another method?

First of all, the syntax is a bit easier than the clip-path one so this can improve the readability of the code as we have fewer values to deal with. But the most important advantage is that we can add a border to the shape! Adding borders to custom shapes is always a nightmare but corner-shape made it easy.

This is logical since, by default, when we add border-radius, the border and other decorations such as box-shadow will follow the rounded corners. It’s still the case even if we change the shape of the corner.

Five shapes with different corner styles labeled: round, scoop, bevel, notch, and squircle, all displayed on a purple background.

Here are the rhombus and octagon shapes with borders:

Note how we can have a border-only version if we keep the background transparent and we can also apply the shape to an image as well. Cool, right?

Hexagon

Do you see how can we achieve a hexagon shape? Try to think about it before reading my explanation.

The trick is to rely on the fact that border-radius accepts vertical and horizontal values, something we always forget about. Let’s take the rhombus shape and decrease the vertical or the horizontal radius.

Do you see that? We have an “almost” hexagon shape. All that is missing is the correct aspect-ratio.

.hexagon {
  border-radius: 50% / 25%; /* OR 25% / 50% */
  corner-shape: bevel;
  aspect-ratio: cos(30deg); /* OR 1/cos(30deg) */
}

We can definitely say that we have the easiest and simplest way to create hexagon shapes!

Triangles

Following the same logic we can create most of the common shapes and triangles aren’t an exception. Again, we can use the Rhombus as a starting point and adjust either the horizontal or the vertical radius like below.

This one can be a bit tricky at first glance because we don’t have the 4 diagonal lines for each corner like the previous shapes but don’t forget that we can use 0 with border-radius which will disable the corresponding corner.

.triangle {
  border-radius: 50% / 100% 100% 0 0; 
  corner-shape: bevel;
} 

From there, we can easily get any kind of triangle by trying the different radius combinations and also playing with aspect-ratio.

Below is a demo with many examples. Try to create some of them before checking my code. It’s the perfect exercise to practice with corner-shape.

The only caveat with triangle shapes is that the border is not perfect. It may sound like a bug but it’s not. I won’t detail the logic behind this but if you want to add a border, you may need a different thickness for one or many sides.

Here is an example with one of the triangle shapes where I am increasing the thickness of the top border a little to have a perfect-looking shape.

As you can see in the code, there is a math formula to get the correct thickness but since it will be a different formula for each triangle shape, I won’t bother you with a boring explanation. Plus you can easily (and rapidly) get a good result with some trial & error. No need to be very precise.

Slanted edge

All the shapes we created rely on percentage values but border-radius accepts length as well, which means we can have elements with variable size but the shape remains static.

Example with a slanted edge where the slant keeps the same size whatever the element width:

The code is a simple as:

.slanted {
  border-top-right-radius: 80px 100%;
  corner-shape: bevel;
}

No need for the shorthand property in this case since only the top-right corner matters. As for the value, I think it’s self-explanatory. Simply notice that there is no / to separate the horizontal and vertical radius when using the longhand properties.

Arrow-like box

Using the same logic we can have an arrow-like box:

.arrow {
  border-radius: 80px / 0 50% 50% 0;
  corner-shape: bevel;
}

Trapezoid & Parallelogram

Also trapezoid & parallelogram shapes:

.trapezoid {
  border-radius: 80px / 100% 0 100% 0;
  corner-shape: bevel;
}
.parallelogram {
  border-radius: 80px / 100% 100% 0 0;
  corner-shape: bevel;
}

Conclusion

Using only the bevel option of corner-shape we were able to create a lot of different shapes easily. All we had to do was to play with border-radius, a well-known property. Not to mention the fact that we can easily add borders and box shadows which is a real game changer compared to shapes created using clip-path or mask.

I will end the article with a last demo where I combine bevel and notch to create an arrow. Yes, you can have a different shape per corner!

What about you? Can you think about a cool shape using corner-shape?

]]>
https://frontendmasters.com/blog/drawing-css-shapes-using-corner-shape/feed/ 8 6235
Creating Blob Shapes using clip-path: shape() https://frontendmasters.com/blog/creating-blob-shapes-using-clip-path-shape/ https://frontendmasters.com/blog/creating-blob-shapes-using-clip-path-shape/#respond Mon, 19 May 2025 14:30:05 +0000 https://frontendmasters.com/blog/?p=5861 After the flower shapes, let’s move to one of the coolest shapes: the Blob! Those distorted wiggly circles that were almost impossible to achieve using CSS. But now, they are possible using the new shape() function.

Article Series

Before we start, take a look at my blob shape generator. Unlike the flower shapes, blobs have the random factor so having a generator to get the code is a lifesaver. This said, stay with me to understand the logic behind creating them, maybe you will want to make your own generator of blobs.

For this shape, we are going to rely on the curve command, so let’s start by understanding how it works.

At the time of writing, only Chrome, Edge, and Safari have the full support of the features we will be using.

The curve Command

This command allows you to draw Bézier curves between two points. With the arc command, we needed a radius, but here we need control points. We can either have one control point and create a Quadratic curve or two control points and create a Cubic curve.

Here is a figure to illustrate a few examples. The black dots illustrate the control points, and the blue ones the starting and ending points.

Illustration showing the difference between creating curves with one control point and two control points using Bézier curves. The top left and bottom left demonstrate the one control point method, while the top right and bottom right illustrate the two control points method.

And a demo:

I won’t detail the exact geometry behind the curves, but notice their behavior close to the starting and ending points. The curve is tangent to the lines that link the starting and ending points with the control points. This will be the key to create our blob shape.

The code of this command is:

clip-path: shape(from Xa Ya, curve to Xb Yb with Xc1 Yc1 / Xc2 Yc2) 

And I will be using one control point, so we can omit the second control point:

clip-path: shape(from Xa Ya, curve to Xb Yb with Xc1 Yc1) 

By combining many curves, we can create a blob. We have to understand how to correctly combine them, so it’s time for a small geometry course.

The Geometry of The Blob

Mathematically speaking, there is no specific geometry for a blob because it’s not a shape we can formally define. We can implement a blob using different methods and calculations, so what I am going to share is my own implementation. It’s probably not the best one, but it gives a good result.

We first start by placing N points around a circle. The number of points is the first parameter of the blob that I am calling “granularity” in my generator. Then I define a distance D I call the depth.

Now, we randomly move the points within the area defined by the distance D. For each point, we pick a random value in the range [0 D] and we make it closer to the center using that value.

For the next step, we take two consecutive points, draw a line between them, and then place a new point at the center. This will double the number of points.

The last step is to draw the Bézier curves. The new points (the blue ones) are the starting and ending points, and the initial points (the black ones) are the control points.

The fact that two adjacent curves share the same tangent is what gives us a continuous and smooth shape, a perfect blob!

That’s it. Now let’s translate this into code.

The Code of The Blob

Similar to the flower shape, the code will be a bunch of curve commands like below:

.blob { 
  clip-path: shape(from X0 Y0, 
     curve to X1 Y1 with Xc1 Yc1,
     curve to X2 Y2 with Xc2 Yc2,
     curve to X3 Y3 with Xc3 Yc3,
     curve to X4 Y4 with Xc4 Yc4,
     ...
     curve to Xn Yn with Ycn Ycn
  )
}

[Xi, Yi] are the starting/ending points (the blue ones), and [Xci, Yci] are the control points (the black ones). For the sake of simplicity, I will use pseudo-code to illustrate the calculation. The real implementation can be done using JavaScript like in my generator, or using Sass (I will share a demo using Sass later).

We first start by defining the control points:

N = 15  /* number of points (granularity) */
D = 20% /* depth */

for i in [1 N] {
R = 50% - random(D);
Xci = 50% + R*math.cos(360deg * i/N));
Yci = 50% + R*math.sin(360deg * i/N));
}

R will define the distance of the points from the center, and it will have a random value between 50% and 50% - D.

Then we define the main points where each one is placed at the center of two consecutive control points:

for i in [1 N] {
Xi = (Xci + Xci+1)/2;
Yi = (Yci + Yci+1)/2;
}

Finally, the shape function will be as follows:

clip-path: shape(from X0 Y0, 
for i in [1 N] {
curve to Xi Yi with Xci Yci,
}
)

Here is a Sass implementation:

One observation we can make is that the shape is responsive. It’s designed to work with square elements (aspect-ratio: 1), but the result is not bad for rectangular elements as well. Resize the element in the demo below and see how the shape behaves:

The code can also be tweaked to create more variations. We can, for example, have a kind of wavy circle by removing the random part and applying a fixed distance to half the points.

Can you think of other variations?

Animating The Blob

Having the blob shape in CSS is already a cool feature. It’s one line of code that can be applied to any element, including images, and it’s responsive! In addition to this, we can easily animate them. The only requirement is to have the same structure inside shape(). So if we take two blobs having the same number of curve commands, then we can animate one into another!

Here is an example where we keep the same number of points and only adjust the depth:

You copy both codes from the generator, apply a transition, and you have a cool hover effect that transforms a circle into a blob!

The bouncing effect you get is made with the linear() function which is another cool feature for custom easing. I am getting the code from here.

Now, if you update the Shape ID and still keep the same number of points, you can have a transition between two different blobs.

Cool, right? The code may look complex but in the end everything is generated for you, so it’s nothing but a few clicks to get a fancy shape with a nice animation! Speaking about animation, let’s end with a demo using a keyframes instead of a transition.

Conclusion

I hope you enjoyed this shape() exploration through this series of articles. Once this feature becomes widely supported, it will be a game changer and we can forget about all the hacky workarounds to create CSS shapes.

Don’t forget to keep an eye on my CSS Shapes and CSS Generators websites from where you can easily copy the code of any CSS shape.

Article Series

]]>
https://frontendmasters.com/blog/creating-blob-shapes-using-clip-path-shape/feed/ 0 5861
Creating Flower Shapes using clip-path: shape() https://frontendmasters.com/blog/creating-flower-shapes-using-clip-path-shape/ https://frontendmasters.com/blog/creating-flower-shapes-using-clip-path-shape/#respond Mon, 12 May 2025 14:51:36 +0000 https://frontendmasters.com/blog/?p=5824 In a previous article, we used modern CSS features such as mask, trigonometric functions, and CSS variables to create flower-like shapes.

The HTML code was a single element, which means we can apply the CSS to image elements and get cool frames like the demo below:

In this article, we are redoing the same shapes using the new shape() function, which I think will become my favorite CSS feature.

At the time of writing, only Chrome, Edge, and Safari have the full support of the features we will be using.

Article Series

What is shape()?

You are probably familiar with clip-path: polygon(), right? A function that allows you to specify different points, draw straight lines between them and create various CSS shapes (I invite you to check my online collection of CSS shapes to see some of them). I said “straight lines” because when it comes to curves, clip-path is very limited. We have circle() and ellipse(), but we cannot achieve complex shapes with them.

shape() is the new value that overcomes such limitation. In addition to straight lines, it allows us to draw curves. But If you check the MDN page or the specification, you can see that the syntax is a bit complex and not easy to grasp. It’s very similar to SVG path, which is good as it gives us a lot of options and flexibility, but it requires a lot of practice to get used to it.

I will not write a boring tutorial explaining the syntax and all the possible values, but rather focus on one command per article. In this article, we will study the arc command, and the next article will be about the curve command. And, of course, we are going to draw cool shapes. Otherwise it’s no fun!

The arc Command

This command allows you to draw an elliptic arc between two points but I will only consider the particular case of a circle which is easier and the one you will need the most. Let’s start with some geometry to understand how it works.

If you have two points (A and B) and a radius, there are exactly two circles with the given radius that intersect with the points. The intersection of both circles gives us 4 possible arcs between A and B that we can identify with a size (small or large) and a direction (clockwise or counter-clockwise)

The code will look like the below:

clip-path: shape(from Xa Ya, arc to Xb Yb of R [large or small] [cw or ccw])

The first command of a shape is always a from to give the starting point, and then we use the arc to define the ending point, the radius, the size, and the direction.

Here is a demo to illustrate the different values:

The points and the radii are the same. I am only changing the size and direction to choose one of the four possibilities. It should be noted that the size and direction aren’t mandatory. The defaults are small and ccw.

That’s all: we have what we need to draw flower shapes!

Creating The Flower Shape

I will start with a figure from the previous article.

Using the mask method, we had to draw a big circle at the center and small circles placed around it. Using shape(), we need to draw the arcs of the small circles. The starting and ending points of each arc are placed where the circles touch each other.

The code will look as follows:

.flower { 
  clip-path: shape(from X0 Y0, 
     arc to X1 Y1 of R,
     arc to X2 Y2 of R,
     arc to X3 Y3 of R,
     arc to X4 Y4 of R,
     ...
     arc to Xn Yn of R
  )
}

And like I did with the mask method, I will rely on Sass to create a loop.

$n: 10;

.flower {
  $m: ();
  $m: append($m,from X0 Y0,comma);
  @for $i from 1 through $n {
    $m: append($m,arc to Xi Yi of R,comma);
  } 
  clip-path: shape(#{$m});
}

Now we need to identify the radius of the small circles (R) and the position of the different points (Xi, Yi). I already did the calculation of the radius in the previous article, so I will reuse the same value:

R = 50%/(1 + 1/math.sin(180deg/$n))

For the points, they are placed around the same circle so their coordinate can be written like below:

Xi = 50% + D*math.cos($i*360deg/$n)
Yi = 50% + D*math.sin($i*360deg/$n)

Here is a figure to illustrate the distance D and the radius R:

I know you don’t want a boring geometry course so here is the value of D.

D = 50%/(math.tan(180deg/$n) + 1/math.cos(180deg/$n))

And the final code will be:

$n: 10;

.flower {
  $m: ();
  $r: 50%/(1 + 1/math.sin(180deg/$n));
  $d: 50%/(math.tan(180deg/$n) + 1/math.cos(180deg/$n));
  $m: append($m,from 
    50% + $d*math.cos(0) 
    50% + $d*math.sin(0),comma);
  @for $i from 1 through $n {
    $m: append($m,arc to 
      50% + $d*math.cos($i*360deg/$n)
      50% + $d*math.sin($i*360deg/$n)
      of $r,comma);
  } 
  clip-path: shape(#{$m});  
}

Wait, we get the inverted shape instead? Why is that?

We didn’t define the size and the direction of the arcs so by default the browser will use small and ccw. This gives us the inverted version of the flower. If you try the 4 different combinations (including the default one) you will get the following:

small ccw and large cw give us the shape we are looking for. The small cw is an interesting variation, and the large ccw is a funny one. We can consider a CSS variable to easily control which shape we want to get.

Combining Both Shapes

Now, what about the shape below?

The idea is to use the same code and alternate between two arc configurations.

$n: 10;

.flower {
  $m: ();
  $r: 50%/(1 + 1/math.sin(180deg/$n));
  $d: 50%/(math.tan(180deg/$n) + 1/math.cos(180deg/$n));
  $m: append($m,from 
    50% + $d*math.cos(0) 
    50% + $d*math.sin(0),comma);
  @for $i from 1 through $n {
    $c:small ccw;
    @if $i % 2 == 0 {$c:large cw}
    $m: append($m,arc to 
      50% + $d*math.cos($i*360deg/$n)
      50% + $d*math.sin($i*360deg/$n)
      of $r $c,comma);
  } 
  clip-path: shape(#{$m});  
}

I introduced a new variable $c within the loop that will have the value small ccw when $i is odd and large cw otherwise.

Cool right? The compiled code will look like the below:

clip-path: 
shape(from X0 Y0,
arc to X1 Y1 of R small ccw,
arc to X2 Y2 of R large cw,
arc to X3 Y3 of R small ccw,
arc to X4 Y4 of R large cw,
...
)

The first arc will use the inner curve (small ccw), the next one the outer curve (large cw) and so on. We repeat this to get our wavy-flower shape!

Optimizing The Code

We made a generic code that allow us to get any shape variation by simply controlling the size/direction of the arcs, but for each particular case, we can have a more simplified code.

For the inverted variation (small ccw), the value of D can be replaced by 50%. This will simplify the formula and also increase the area covered by the shape. We also need to update the value of the radius.

$n: 10;

.flower {
  $m: ();
  $r: 50%*math.tan(180deg/$n);
  $m: append($m,from 
    50% + 50%*math.cos(0) 
    50% + 50%*math.sin(0),comma);
  @for $i from 1 through $n {
    $m: append($m,arc to 
      50% + 50%*math.cos($i*360deg/$n)
      50% + 50%*math.sin($i*360deg/$n)
      of $r,comma);
  } 
  clip-path: shape(#{$m});  
}

We can do the same for the main shape, but this time we can simplify the value of the radius and use 1%.

$n: 10;

.flower {
  $m: ();
  $d: 50%/(math.cos(180deg/$n)*(1 + math.tan(180deg/$n)));
  $m: append($m,from 
    50% + $d*math.cos(0) 
    50% + $d*math.sin(0),comma);
  @for $i from 1 through $n {
    $m: append($m,arc to 
      50% + $d*math.cos($i*360deg/$n)
      50% + $d*math.sin($i*360deg/$n)
      of 1% cw,comma);
  } 
  clip-path: shape(#{$m});  
}

This one is interesting because using 1% as a radius is kind of strange and not intuitive. In the explanation of the arc command, I said that we have exactly two circles with the given radius that intersect with the two points, but what if the radius is smaller than half the distance between the points? No circles can meet that condition.

This case falls into an error handling where the browser will scale the radius until we can have at least one circle that meets the conditions (yes, it’s defined within the specification). That circle will simply have its radius equal to half the distance between both points. It also means we only have two arcs with the same size (small and large will be equal)

In other words, if you specify a small radius (like 1%), you are telling the browser to find the radius value for you (a lazy but clever move!). This trick won’t work in all the situations but can be handy in many of them so don’t forget about it.

Conclusion

We are done with this first article! You should have a good overview of the arc command and how to use it. I only studied the particular case of drawing circular arcs but once you get used to this you can move to the elliptic ones even if I think you will rarely need them.

Let’s end with a last demo of a heart shape, where I am using the arc command. Can you figure out how to do it before checking my code?

And don’t forget to bookmark my online generators from where you can get the code of the flower shapes and more!

Article Series

]]>
https://frontendmasters.com/blog/creating-flower-shapes-using-clip-path-shape/feed/ 0 5824
shape(): A New Powerful Drawing Syntax in CSS https://frontendmasters.com/blog/shape-a-new-powerful-drawing-syntax-in-css/ https://frontendmasters.com/blog/shape-a-new-powerful-drawing-syntax-in-css/#comments Wed, 07 May 2025 14:20:49 +0000 https://frontendmasters.com/blog/?p=5662 that we absolutely needed.]]> I first saw in the Safari 18.4 release notes that shape(), a new function is now supported. Then I saw on MDN it’s actually already in Chrome, too!

The shape() function joins friends like polygon(), circle(), rect(), inset(), and a handful of others. These functions are used as values for a handful of things in CSS, namely:

  • clip-path — Clipping away parts of elements
  • offset-path — Moving elements along a path
  • shape-outside — Applied to a float-ed element such that content flows along the path

Fair warning: shape() only seems to work with clip-path. I couldn’t find a ton of information on this, but the Chrome blog does state it. It will probably work with the other properties in due time.

Let’s focus on clip-path here which I might argue is the most useful anyway, as it makes an entire element into the shape described which feels like a more generally applicable thing.

I got into this on the CodePen blog where I equated shape() to <path d=""> in SVG, which is surely the intention. You can actually set the d attribute in CSS, but it only works on <path> elements, and the unitless values translate only to pixels, which makes it not particularly CSSy or useful.

One situation I mentioned was Trys Mudford’s blog post where this was the design situation at hand:

Oh look, a use case.

Those light yellow boxes are basically polygons with rounded corners. In a perfect world, polygon() could do this with the round keyword, as specced, but alas that doesn’t work just yet. But because shape() is essentially all-powerful, that does work now (in Chrome and Safari anyway, and this feels like a decently progressive-enhancement thing).

Temani Afif saw that and did the work!

This is very awesome. This is quite the power tool for shape-making. I think we’re going to see a lot of fancy stuff come out of this.

It’s true we already have a path() function, but remember, it’s sooooo limited. The values are only pixels, which are some pretty big handcuffs in a responsive world full of intrinsic content (that is, elements on the web that respond to their contents and environment). Simon Fraser on the WebKit blog introduces this new feature and calls it out:

… using path() in clip-path can’t be responsive; you can’t write CSS rules so that the path adapts to the size of the element. This is where the new shape() function comes in.

Coincidentally, Simon’s demo (Jen’s demo?) also shows off an arrow shape:

That’s using multiple different drawing commands (line and arc, but there are more), keywords like top and left (excellent, but I wonder why logical properties don’t work?), and, even more deliciously, container units (e.g. cqh). The orange border there is a good reminder that clip-path, well, clips. So it’ll lop off anything at all on this element in those areas, including content.

Noam Rosenthal got in on the fun over on the Chrome for Developers blog, underscoring just how hard this stuff used to be:

clip-path: shape() lets you clip your element using arbitrary and responsive shapes, previously only possible using techniques like conic gradients or JavaScript-constructed SVG.

And like all this good company, absolutely couldn’t resist peppering in other CSS goodness into a demo. His demo here uses different drawing commands than we’ve seen so far, custom properties (which are an extremely natural fit), and even animation (!!).

I see Temani is hard on the case with a blob generator using shape(), which, I believe as long as there are the “same number of points”, can be animated by changing the clip-path entirely. Like:

And obviously I love this:

The Actual Shape Commands

The spec covers them, but the best writeup I’ve seen is Geoff’s on CSS-Tricks. He’s got a bit more detail so check that out, but here’s the list:

  • line
  • vline
  • hline
  • arc
  • curve
  • smooth

Each of them have a bit of sub-syntax to themselves. Like the curve command might look like curve to 50% 50% with 50% 0 which would continue drawing the shape to the exact center of the element in a curve in which the top center is a “control point” and so curves in that direction.

In my experience it’s quite easy to make a small mistake in the syntax and wreck the whole thing. But hey that’s understandable.

Squircles with shape()

I get to have some fun too! It occurred to me that digital designs most elusive beast, the squircle, might be now achievable with reasonable normal web tech.

SVG can do it, but I wouldn’t call it particularly readable code. “Monoco is a tiny JavaScript library that adds squircles” (via SVG background images) and it does a pretty good job of it I’d say, but that’s more technology than I normally like to throw at something like this. Jared White by way of Simeon Griggs has a pretty nice SVG-based solution as well, leveraging SVG-as-clip-path.

I like how relatively chill that SVG path is, but still, shape() can allow us to squish this down into just CSS which is kinda sweet.

That is… if I was fully smart enough to do it.

I crudely drew one in Figma so that I could label the points for writing the syntax out.

I figured if I just did a curve to every one of those points with control points a bit the edges, it would… work? So basically like this:

div {
  clip-path: shape(
    from 5% 3%,
    curve to 95% 3% with 50% 0,
    curve to 97% 5% with 97% 3%,
    curve to 97% 95% with 100% 50%,
    curve to 95% 97% with 97% 97%,
    curve to 5% 97% with 50% 100%,
    curve to 3% 95% with 3% 97%,
    curve to 3% 5% with 0% 50%,
    curve to 5% 3% with 3% 3%,
  );
}

Which basically works. I tried playing around with arc and smooth instead but couldn’t manage to make it any better (with my like zero geometry skills). Then instead of hard coding those percentage values, I made them in custom properties with sliders to squiggle them around a little.

It’s a little janky — but I trust someone make like a real quality geometrically sound version eventually.


Update #1

I heard from Peter Herbert over email:

I found a somewhat more accurate version of the iOS squircle. Apparently the Apple squircle uses three cubic beziers in each corner. The original research that figured out the curves I found here, and I used Claude to find the points.

Update #2

Matthew Morete commented below with a tool he made that converts SVG path commands into shape() commands, which is awesome. Squircles are one of the provided demos, and the commands are very chill:

.squircle {
  clip-path: shape(
    from 0% 50%, 
    curve by 50% -50% with 0% -45% / 5% -50%, 
    smooth by 50% 50% with 50% 5%, 
    smooth by -50% 50% with -5% 50%, 
    smooth by -50% -50% with -50% -5%
  );
}
]]>
https://frontendmasters.com/blog/shape-a-new-powerful-drawing-syntax-in-css/feed/ 7 5662
Creating Flower Shapes using CSS Mask & Trigonometric Functions https://frontendmasters.com/blog/creating-flower-shapes-using-css-mask-trigonometric-functions/ https://frontendmasters.com/blog/creating-flower-shapes-using-css-mask-trigonometric-functions/#respond Thu, 29 Feb 2024 15:04:12 +0000 https://frontendmasters.com/blog/?p=1021 Creating unusual shapes is always a fun exercise and a good way to practice your CSS skills. One might argue that SVG is better for this job, but nowadays we have a lot of new CSS tricks that allow us to create shapes with a clean and optimized code. Through this two-article series, we will explore what can be done with CSS nowadays.

Article Series

In this article, we are going to create flower-like shapes. We are going to rely on modern features like mask, trigonometric functions, CSS variables, etc.

four flower-like shapes with purple to pink gradients. two of them have petals and two of them have spikes. two of them are filled and two of them are outlined.

Before we start, you can take a look at my online generator for flower shapes to get an overview of what we are building here. You can easily define your settings and get the CSS code in no time. Some of the code we will be writing can be complex so it’s always good to have a generator to make our life easy. That said I invite you to keep reading to understand the logic behind the code you are copying and be able to tweak it if needed. 

The Geometry of A Flower Shape

The structure of the flower shape can be seen as small circles placed around a bigger one. 

Consider that the small circles touch each other without overlapping each other. This will make the calculation a bit challenging as we will need accurate formulas. Turns out this is a perfect use case for trigonometric functions in CSS.

The shape can be controlled using 2 variables; the number of small circles (N) and their radius (R). From there we can identify the size of the whole shape and the radius of the big circle.

Here is a figure to illustrate some of the values and from where we can extract the different formulas.

geometry of the circles showing the radius comparisons.

I won’t start a boring geometry course so I will jump straight to the formulas we need to use. The size of the element is equal to

2 * (X + R) = 2 * (R/sin(180deg/N) + R) = 2 * R * (1 + 1/sin(180deg/N))

and the radius of the big circle is equal to

C  = R/tan(180deg/N)

We have all that we need to start writing some code.

Coding The Shape

The main challenge is to rely on a single element. We are not going to consider a complex HTML structure where each circle is a different element. Instead, we will only use one element (and no pseudo-elements either!)

I mentioned mask, we’ll be using that, and gradients will allow us to do the shape drawing we want to do. Since it’s all about circles we are going to use radial-gradient(). We will also use a bit of Sass (for the looping feature) to control the number of circles. The number of gradients will depend on the number of circles and with Sass we can write a loop to generate the needed gradients.

Let’s start by setting the different variables and the shape size:

$n: 12; /* the number of circles/petals */

.flower {
  --r: 30px; /* the radius of the small circles */
  width: calc(2*var(--r)*(1 + 1/sin(180deg/#{$n})));
  aspect-ratio: 1;
  mask: radial-gradient(#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0);
  background: /* you background coloration */
}

Nothing fancy so far, we simply translated the previous formulas using code. I also added the big circle so all that we are missing is the small ones. The Sass code to generate them will look like this:

$m: (); /* empty variable */
@for $i from 1 through ($n) { /* loop through the number of circles*/
  $m: append($m, 
    radial-gradient(50% 50%,#000 100%,#0000) no-repeat
    x y / calc(2*var(--r)) calc(2*var(--r)), 
  comma);
}

--r defines the radius so the size of each gradient will be equal to calc(2*var(--r)). Then we need to identify the position of each gradient (the x y).

Here as well, we need to consider some geometry formulas

x = calc(50% + 50%*cos(360deg*#{$i/$n})) 
y = calc(50% + 50%*sin(360deg*#{$i/$n}))

The final code will be:

$n: 6; /* the number of circles/petals */

.flower {
  --r: 30px; /* the radius of the small circles */
  width: calc(2*var(--r)*(1 + 1/sin(180deg/#{$n})));
  aspect-ratio: 1;
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#000 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: radial-gradient(#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0),{$m};
  background: /* you background coloration */
}

Note how the mask property takes the value generated using Sass in addition to the gradient that creates the big circle.

Our shape is done!

The size of the shape is controlled by the radius of the small circles but we can do the opposite which is probably more convenient since we generally want to control the width/height of our element.

.flower {
  --w: 200px; /* the size */
  --r: calc(var(--w)/(2*(1 + 1/sin(180deg/#{$n}))));
  width: var(--w);
  /* same as before */
}

We can even optimize the previous code a little and get rid the of --w variable. The latter is defining the width/height of the element and gradients can access such value using percentages we can write the code like below:

$n: 12; /* the number of circles/petals */

.flower {  
  width: 300px; /* the size */
  --r: calc(50%/(1 + 1/sin(180deg/#{$n}))); /* using percentage instead of --w */
  aspect-ratio: 1;
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#000 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0),#{$m};
}

Now by adjusting the width you control the size of the whole shape. Here is an interactive demo where you can resize the element and see how the shape adjusts automatically. Try it below with the resizer handle on the bottom right of the box:

Voilà! We did a nice flower shape without hack or complex code and you can easily control it by adjusting a few variables. You either use the above code or you consider my online generator to get the generated CSS without the variables and Sass.

Inverting the shape

Let’s try a different shape this time. It’s somehow the invert of the previous one where the circular part is going inside instead of outside. Well, a figure worth a thousand words.

spiky flower shape with purple to orange coloring

​​The code to get the above shape is the same as the previous one, but we are going to introduce mask-composite​. The idea is to cut the small circles from the bigger one which translates to a “subtract” composition.

Here is a figure to illustrate the process:

The code of mask will look like this:


mask: 
 radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0) subtract,
 #{$m};

And here is the one of the previous shape to compare both:


mask: 
 radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0),
 #{$m};

The only difference is the value of composition “subtract”. And since the remaining code is also the same, we can introduce a variable to control which one to use:


$n: 12; /* the number of circles/petals */

.flower {
  width: 300px;
  aspect-ratio: 1;
  --r: calc(50%/(1 + 1/sin(180deg/#{$n})));
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#000 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0) var(--alt,),#{$m};
}
.alt {
  --alt: subtract;
}

The var(--alt,) will default to nothing when the variable is not specified and we get the code of the first shape. By adding the composition value, we get the second shape. Two different shapes with the same code.

Why not simply add mask-composite: subtract?

This won’t work because it will apply a “subtract” composition between all the gradient layers whereas we want the composition to happen only between the big circle and the small ones. If you want to use mask-composite it should have the following value:

mask-composite: subtract, add, add, ..., add;

We perform and “add” composition between the small circles (the default composition) then we “subtract” the result from the big circle. It’s clear that adding one value inside the shorthand version is easier.

The Border-Only Version

Now let’s tackle the border-only version of the previous shapes. We are also going to rely on mask-composite and the same code structure.

First of all, let’s introduce a new variable to control the border thickness and update the code that generates the small circles.

$n: 12; /* the number of circles/petals */

.flower {
  --b: 10px; /* the border thickness*/

  width: 300px;
  aspect-ratio: 1;
  --r: calc(50%/(1 + 1/sin(180deg/#{$n})));
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#0000 calc(100% - var(--b)),#000 0 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: #{$m};
}

Nothing complex so far. Instead of full circles, we are getting a border-only version. This time we don’t want them to touch each other but rather overlap a bit to have a continuous shape.

We need to increase the radius a little from this

--r: calc(50%/(1 + 1/sin(180deg/#{$n})));

To this:

--r:calc((50% + var(--b)/(2*sin(180deg/#{$n})))/(1 + 1/sin(180deg/#{$n})));

Again some geometry stuff but you don’t really need to accurately understand all the formulas. I did the hard work to identify them and you only need to understand the main trick. In the end, all you have to do is update a few variables to control the shape or get the code from my generator.

The result so far:

Now, we use mask-composite with another gradient to hide some parts and get our final shapes. Here is a figure to illustrate the process for both shapes.

For the first shape:

And for the second one:

In both cases, I am introducing an extra gradient that will intersect with the small circles. The difference between each shape is the coloration of the extra gradient. In the first case, we have transparent inside and filling outside and for the second case, we have the opposite.

/* first shape */
mask:
 radial-gradient(100% 100%,#0000 calc(var(--r)/tan(180deg/#{$n}) - var(--b)/(2*tan(180deg/#{$n}))),#000 0) intersect, 
 #{$m};
/* second shape */
mask:
 radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n}) - var(--b)/(2*tan(180deg/#{$n}))),#0000 0) intersect, 
 #{$m};

And here is the full code with both variations:

If you have some trouble visualizing how mask-composite works, I invite you to read the crash course written by Ana Tudor where you will find a more in-depth exploration.

One More Shape

Another flower? Let’s go!

blob like shape, sort of like a gear with smoothed over cogs.

This time, it’s your turn to figure out the code. Consider this as a small piece of homework to practice what we have learned together. As a hint, here is a figure that illustrates the mask-composite you need to perform, or maybe you will figure out another idea! If so don’t hesitate to share it in the comment section

Here is the code of my implementation but make a try before checking it so you can compare your method with mine. Take the time to study this last example because it will be the starting point of our second article.

Conclusion

I hope you enjoyed this little experimentation using CSS mask and trigonometric functions. You are probably not going to use such shapes in a real project but creating them is a good way to learn new features. If you have to remember one thing from this article it’s the use of mask-composite. It’s a powerful property that can help you create complex shapes.

It is worth noting that since we are using mask, we can easily apply our shape to image elements.

Don’t forget to bookmark my flower shapes generator so you can easily grab the code whenever you need it. I also have more CSS generators that invite you to check. Most of them rely on CSS mask as well and I have a detailed article linked to each one.

I will close this article with a few mesmerizing animations involving some flower shapes.

Article Series

]]>
https://frontendmasters.com/blog/creating-flower-shapes-using-css-mask-trigonometric-functions/feed/ 0 1021