Improving CSS Shapes with Trigonometric Functions

CSS Trigonometric functions are supported in the latest versions of Safari, Firefox, Edge, and Chrome. We also discuss animation via @property, which is supported in the latest Safari, Edge, and Chrome (as of this writing).

The CSS Shapes specification enabled a lot to make interesting shapes on the web today, via clip-path, shape-outside, and more. With the introduction of CSS Trigonometric functions, we can simplify how we make regular polygons and build even more complex shapes by more easily approximating curves.

Even though what we discuss will apply to more, we will focus this discussion applying trigonometry to the clip-path property. We will start with an overview of how to work with basic clip paths and a quick discussion on the math side of trigonometry. However, you can always skip to where we combine trigonometric functions and clip paths and dig into the demos.

CSS Shapes Basics 

With clip paths and Basic Shapes functions, we can take our rectangular elements and define a shape where only portions of our element will be visible. There are a few key functions, but circle(), ellipse(), and polygon() are likely the most straightforward ways to clip to basic shapes.

As the names state, we can create rounded clips via circle and ellipse, and we are able to clip our element to any polygon shape with polygon() by passing in a collection of coordinates. Any valid length or percentage can be used to define the radius and center points for circles and ellipses, as well as for coordinates within polygons.

/* circle with a 2rem radius, at center of element */
clip-path: circle(2rem);

/* circle with a 20% radius, centered on the right side, offset slightly at the top*/
clip-path: circle(20% at 100% 1rem);

/* tall ellipse, at center of element */
clip-path: ellipse(20% 50%);
/* rectangle */
clip-path: polygon(
0% 20%,
100% 20%,
0% 60%,
100% 60%
);
/* triangle with base aligned to the bottom of element */
clip-path: polygon(
50% 0%,
0% 100%,
100% 100%
);

See the Pen Clip Path Gallery by Dan Wilson (@danwilson) on CodePen.

As long as you can determine an X,Y coordinate for each point on your polygon, you can build impressive clip-path definitions.

While we get a lot with these basic shape tools, there are still limitations to what can reasonably be built with clip paths (and CSS Shapes in general):

  1. You can only use one shape function (a list of clip path functions to be combined is not supported by the property)
  2. An ellipse can only be vertical or horizontal (no diagonal ellipses or rotations)
  3. You cannot easily create shapes with curves (except for the single circle, single ellipse, or a single rounded rectangle)
  4. To build arbitrary polygons, a non-trivial amount of JavaScript or other calculations might be necessary to end up with properly mapped coordinates

Advancing CSS Shapes with Advanced CSS 

Now, I will admit I forgot almost everything about trigonometry in recent years, but it might be time to relearn some things with the introduction of CSS Trigonometric functions. Using angles, sine, cosine, and more can help with positioning, animations, and... drawing shapes.

For this next example, I took two attempts to make a regular pentagon (a five-sided polygon where each side is of equal length). For the first pentagon, I didn’t use equations, I just typed in 5 different coordinates repeatedly until I got something that was close visually. Once I felt pretty good about it, I took another try using the some of the new trigonometric functions sin() and cos() available in CSS. The second pentagon is in fact a regular pentagon.

See the Pen Clip Path Gallery by Dan Wilson (@danwilson) on CodePen.

Before we dig into the CSS specifics, let’s focus on the mathematics first.

How to create a regular polygon with trigonometric functions 

To make a regular polygon, you can place points equidistantly around a circle and connect them with straight lines. So for a pentagon, we can place five points around a circle with a known radius. Since a circle is 360 degrees around, we can place them at 72 degree intervals (the equation to determine the angle interval is n/360 where n is the number of sides).

But... we are dealing with coordinates and two axes (x and y). So how do we place points around a circle based on an angle and a radius?

In a traditional coordinate system, the function cos(<angle>) helps us determine the x value of our coordinate when combined with a radius. Similarly, sin(<angle>) and the radius will be used to determine the y value.

See the Pen Visual Reference: Traditional Coordinate System by Dan Wilson (@danwilson) on CodePen.

The radius of our circle becomes the hypotenuse of a right triangle (don’t you just love reliving all these mathematical terms?) at any point along the circle’s surface.

To determine x: x = cos(<angle>) * radius

To determine y: y = sin(<angle>) * radius

A regular pentagon then becomes a collection of five coordinates, where the angles used in the trigonometric functions are 72 degrees apart. For a radius of 1cm:

(cos(0deg) * 1cm) (sin(0deg) * 1cm),
(cos(72deg) * 1cm) (sin(72deg) * 1cm),
(cos(144deg) * 1cm) (sin(144deg) * 1cm),
(cos(216deg) * 1cm) (sin(216deg) * 1cm),
(cos(288deg) * 1cm) (sin(288deg) * 1cm)

How to create a regular polygon with CSS 

The hard work is behind us and now we can discuss how to use this in CSS.

As far as CSS and trigonometry are concerned, our previous example is valid CSS once we put it inside a polygon() and some calc()s:

clip-path: polygon(
calc(cos(0deg) * 1cm) calc(sin(0deg) * 1cm),
calc(cos(72deg) * 1cm) calc(sin(72deg) * 1cm),
calc(cos(144deg) * 1cm) calc(sin(144deg) * 1cm),
calc(cos(216deg) * 1cm) calc(sin(216deg) * 1cm),
calc(cos(288deg) * 1cm) calc(sin(288deg) * 1cm)
)

Those five points give us a similar regular pentagon to our earlier pentagon side-by-side example... almost.

See the Pen Clip Path Gallery (Pentagon) by Dan Wilson (@danwilson) on CodePen.

We have two primary considerations to think about when moving from the traditional coordinate system to one on our device screens

  1. 0,0 on our element is the top left corner (regardless of writing mode, etc.)
  2. The y values goes positive as you move down the axis

So when dealing with clip paths, if we want our shape to be centered in our element, we need to offset our center point by 50%. And depending on the orientation we want, it may be useful to multiply some y values by -1. In this case, our main desire is to get the element centered, so we can modify our definition with:

clip-path: polygon(
calc(50% + cos(0deg) * 1cm) calc(50% + sin(0deg) * 1cm),
calc(50% + cos(72deg) * 1cm) calc(50% + sin(72deg) * 1cm),
calc(50% + cos(144deg) * 1cm) calc(50% + sin(144deg) * 1cm),
calc(50% + cos(216deg) * 1cm) calc(50% + sin(216deg) * 1cm),
calc(50% + cos(288deg) * 1cm) calc(50% + sin(288deg) * 1cm)
)

Let’s assume for the next few examples (as the CodePen demos do) that our element we are clipping is a square shape. We are still using 1cm so far, but for our pentagon to maximize inside our square element, we really should be using a radius of 50% since that will allow us to use as much space as possible inside our element. To be flexible going forward, we can make it straightforward to change our center point and radius by using CSS Custom Properties:

--radius: 50%;
--center-x: 50%;
--center-y: 50%;
clip-path: polygon(
calc(var(--center-x) + cos(0deg) * var(--radius)) calc(var(--center-y) + sin(0deg) * var(--radius)),
calc(var(--center-x) + cos(72deg) * var(--radius)) calc(var(--center-y) + sin(72deg) * var(--radius)),
calc(var(--center-x) + cos(144deg) * var(--radius)) calc(var(--center-y) + sin(144deg) * var(--radius)),
calc(var(--center-x) + cos(216deg) * var(--radius)) calc(var(--center-y) + sin(216deg) * var(--radius)),
calc(var(--center-x) + cos(288deg) * var(--radius)) calc(var(--center-y) + sin(288deg) * var(--radius))
)

This is admittedly a fairly long property value to specify five points for a polygon. But we did not have to do any calculations or figure coordinates out visually, we were able to lean on math and put the equations directly in our CSS. And if we want to make small adjustments like change the center point or radius, we do not have to go recalculate all the coordinates and hardcode them.

Another key benefit to using CSS trigonometric functions is that they can be used with any valid CSS angle, whether that is in degrees, radians, turns, or other options. So we could instead define our pentagon in the turn unit:

clip-path: polygon(
calc(var(--center-x) + cos(0turn) * var(--radius)) calc(var(--center-y) + sin(0deg) * var(--radius)),
calc(var(--center-x) + cos(.2turn) * var(--radius)) calc(var(--center-y) + sin(.2turn) * var(--radius)),
calc(var(--center-x) + cos(.4turn) * var(--radius)) calc(var(--center-y) + sin(.4turn) * var(--radius)),
calc(var(--center-x) + cos(.6turn) * var(--radius)) calc(var(--center-y) + sin(.6turn) * var(--radius)),
calc(var(--center-x) + cos(.8turn) * var(--radius)) calc(var(--center-y) + sin(.8turn) * var(--radius))
)

The added flexibility of the additional angle units is refreshing, especially after working with the JavaScript trigonometry equivalents such as Math.sin() which only accept radians for the parameter.

As a bonus, animation also becomes more direct in browsers that support @property (and thus can enable interpolation on the custom property value, independent from the overall clip-path value):

See the Pen Clip Path Gallery (animated) by Dan Wilson (@danwilson) on CodePen.

Approximating rounded and curved shapes 

With the help of trigonometry (and admittedly longer clip path values), we can approximate curves to make even more interesting possibilities

Now that we know we can build regular polygons along a circle with trigonometry, we can use that to make polygons with a large amount of sides to create what ultimately appear to be circles or ellipses. For most elements on a page on a large computer monitor, a 60-sided regular polygon will be more than enough sides to make (an approximation of) a circle. Depending on the size of the element, the straight edges likely will not even be visible, so the curve will appear fairly smooth. If you need to can add more sides to the polygon to make it appear more circular.

See the Pen Approximating a Circle by Dan Wilson (@danwilson) on CodePen.

And with this capability, we can start to do more with circles, curves, straight edges, and mixing them all together.

We are no longer limited to one circle. So we can build a shape that has the appearance of a snowperson, with two circles overlapping each other.

See the Pen Building a Snowperson (as clip-path) by Dan Wilson (@danwilson) on CodePen.

We also can plot out a rotated ellipse (remember that using the ellipse() function will only produce ellipses that are vertical or horizontal). This takes some advanced calculations, but any time there is an equation, it can translate directly to our CSS property value. A little search on the internet led me to an answer to the equations necessary, which then led to the following CSS pattern for each coordinate:

clip-path: polygon(
calc(var(--center-x) + (cos(0deg) * cos(var(--rotation)) * var(--radius-x)) - (sin(0deg) * sin(var(--rotation)) * var(--radius-y)))
calc(var(--center-y) + (cos(0deg) * sin(var(--rotation)) * var(--radius-x)) + (sin(0deg) * cos(var(--rotation)) * var(--radius-y))),
/* followed by many more coordinates */
)

--rotation: .1turn;
--radius-x: 20%;
--radius: 50%;
--center-x: 50%;
--center-y: 50%;

As discussed before, nothing here is brief. I am even still using JavaScript in this case to generate the clip path as a convenience:

function generateRotatedEllipse() {
const points = [];

for (let i = 0; i < 360; i = i + 6) {
points.push(`calc(var(--center-x) + (cos(${i}deg) * cos(var(--rotation)) * var(--radius-x)) - (sin(${i}deg) * sin(var(--rotation)) * var(--radius-y)))
calc(var(--center-y) + (cos(
${i}deg) * sin(var(--rotation)) * var(--radius-x)) + (sin(${i}deg) * cos(var(--rotation)) * var(--radius-y)))`
);
}

const ellipse = document.querySelector(".rotated-ellipse");
ellipse.style.clipPath = `polygon(${points.join(",")})`;
}

But keeping the math calculation in CSS and combine it with CSS Custom Properties gives us a flexible way to build clip paths and update them in the future... or in an animation (as seen in this ellipse demo if your browser supports @property):

See the Pen Spinning Elliptical Reveal (via clip-path and @property) by Dan Wilson (@danwilson) on CodePen.

We have a lot of ways to work with CSS Shapes, and we have a lot more to explore as CSS continues to increase in capabilities.

See the Pen Clip Path Gallery by Dan Wilson (@danwilson) on CodePen.