Squircles: Bringing iOS 7’s solution to rounded rectangles to CSS

James S
8 min readOct 4, 2017

--

In iOS 7, Apple changed their app icons from rounded squares to “squircles”. A lot has already been written on this, but the gist is that the new icons have much smoother and less jarring curves. The problem: css’s border-radius property doesn’t support this. Instead, you can only mask the corner with a circle, leaving you with something that’s kind of ugly.

CSS border-radius only masks the corners with circles. Notice the sharp edge where the curve starts.

Ideally, we would have a smooth transition between edge and corner.

There are no sharp transitions here.

So: how do we do this in CSS? The answer is using the clip-path property. This property allows us to cut away parts of an element and is an all-around pretty useful property to know. In this case, we want to generate the coordinates of a squircle and clip everything outside of it. The CSS will end up looking similar to the octagon below, only with a lot more points.

clip-path: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);

All we’re doing here is supplying clip-path a list of (x, y) coordinates and then the browser hides everything outside of the polygon.

Now that we understand how to clip a polygon, we can create a method of generating (x, y) coordinates for a squircle. Fortunately, Wikipedia provides the functions we need to generate them parametrically using an angle and a radius. We’ll fix the radius at 50 (for 100%, or 50% on each side) and run the function on a set of values between 0 and 360 degrees.

Here’s the first part, an implementation of the squircle function in JavaScript:

const squircle = radius => theta => ({
x: Math.pow(Math.abs(Math.cos(theta)), 2 / radius) * 50 * Math.sign(Math.cos(theta)) + 50,
y: Math.pow(Math.abs(Math.sin(theta)), 2 / radius) * 50 * Math.sign(Math.sin(theta)) + 50
});

I know math-y functions can get tough to read, especially in JS with the Math.pow’s and Math.sin’s everywhere. Essentially, we just translate the parametric equation into JavaScript with the following changes:

  1. We set the radius to 50
  2. We add 50 to the result because the function generates values between -radius and +radius, and we want [0, 2 * radius].
  3. Instead of taking the square root in the first part of the equations, we take Math.pow(n, 2 / radius), where radius is the value we’d pass to border-radius. This seems to control the sharpness of the curve well enough.

Next, we just need to generate a list of angles to generate coordinates for. This is easy enough to do in a couple lines of JavaScript.

(new Array(360))
.fill(0)
.map((x, i) => i)
.map(to_radians) // Defined as deg => deg * Math.PI / 180 elsewhere

This just creates an array of the numbers 0 through 359 and converts them to radians. This means that we’ll have coordinates at every degree, but feel free to mess with the values a little if you want a finer or coarser grain. For smaller elements, you may only need to sample every third or fourth degree, but larger ones might need to be sampled at every degree. For now, I’m just sticking with sampling every degree for the sake of simplicity.

Finally, we can pass these values to our squircle function and process the results into something compatible to CSS.

(new Array(360))
.fill(0)
.map((x, i) => i)
.map(to_radians) // Defined as deg => deg * Math.PI / 180 elsewhere
.map(squircle(4)) // We'll use a border-radius of 4
.map(({ x, y }) => ({ x: Math.round(x * 10)/10, y: Math.round(y * 10)/10 })) // Round to the ones place
.map(({ x, y }) => `${x}% ${y}%`)
.join(', ')

This gives us the following string:

100% 50%, 100% 56.6%, 100% 59.3%, 100% 61.4%, 99.9% 63.2%, 99.9% 64.8%, 99.9% 66.2%, 99.8% 67.5%, 99.8% 68.7%, 99.7% 69.8%, 99.6% 70.8%, 99.5% 71.8%, 99.5% 72.8%, 99.4% 73.7%, 99.3% 74.6%, 99.1% 75.4%, 99% 76.3%, 98.9% 77%, 98.8% 77.8%, 98.6% 78.5%, 98.5% 79.2%, 98.3% 79.9%, 98.1% 80.6%, 98% 81.3%, 97.8% 81.9%, 97.6% 82.5%, 97.4% 83.1%, 97.2% 83.7%, 97% 84.3%, 96.8% 84.8%, 96.5% 85.4%, 96.3% 85.9%, 96% 86.4%, 95.8% 86.9%, 95.5% 87.4%, 95.3% 87.9%, 95% 88.3%, 94.7% 88.8%, 94.4% 89.2%, 94.1% 89.7%, 93.8% 90.1%, 93.4% 90.5%, 93.1% 90.9%, 92.8% 91.3%, 92.4% 91.7%, 92% 92%, 91.7% 92.4%, 91.3% 92.8%, 90.9% 93.1%, 90.5% 93.4%, 90.1% 93.8%, 89.7% 94.1%, 89.2% 94.4%, 88.8% 94.7%, 88.3% 95%, 87.9% 95.3%, 87.4% 95.5%, 86.9% 95.8%, 86.4% 96%, 85.9% 96.3%, 85.4% 96.5%, 84.8% 96.8%, 84.3% 97%, 83.7% 97.2%, 83.1% 97.4%, 82.5% 97.6%, 81.9% 97.8%, 81.3% 98%, 80.6% 98.1%, 79.9% 98.3%, 79.2% 98.5%, 78.5% 98.6%, 77.8% 98.8%, 77% 98.9%, 76.3% 99%, 75.4% 99.1%, 74.6% 99.3%, 73.7% 99.4%, 72.8% 99.5%, 71.8% 99.5%, 70.8% 99.6%, 69.8% 99.7%, 68.7% 99.8%, 67.5% 99.8%, 66.2% 99.9%, 64.8% 99.9%, 63.2% 99.9%, 61.4% 100%, 59.3% 100%, 56.6% 100%, 50% 100%, 43.4% 100%, 40.7% 100%, 38.6% 100%, 36.8% 99.9%, 35.2% 99.9%, 33.8% 99.9%, 32.5% 99.8%, 31.3% 99.8%, 30.2% 99.7%, 29.2% 99.6%, 28.2% 99.5%, 27.2% 99.5%, 26.3% 99.4%, 25.4% 99.3%, 24.6% 99.1%, 23.7% 99%, 23% 98.9%, 22.2% 98.8%, 21.5% 98.6%, 20.8% 98.5%, 20.1% 98.3%, 19.4% 98.1%, 18.7% 98%, 18.1% 97.8%, 17.5% 97.6%, 16.9% 97.4%, 16.3% 97.2%, 15.7% 97%, 15.2% 96.8%, 14.6% 96.5%, 14.1% 96.3%, 13.6% 96%, 13.1% 95.8%, 12.6% 95.5%, 12.1% 95.3%, 11.7% 95%, 11.2% 94.7%, 10.8% 94.4%, 10.3% 94.1%, 9.9% 93.8%, 9.5% 93.4%, 9.1% 93.1%, 8.7% 92.8%, 8.3% 92.4%, 8% 92%, 7.6% 91.7%, 7.2% 91.3%, 6.9% 90.9%, 6.6% 90.5%, 6.2% 90.1%, 5.9% 89.7%, 5.6% 89.2%, 5.3% 88.8%, 5% 88.3%, 4.7% 87.9%, 4.5% 87.4%, 4.2% 86.9%, 4% 86.4%, 3.7% 85.9%, 3.5% 85.4%, 3.2% 84.8%, 3% 84.3%, 2.8% 83.7%, 2.6% 83.1%, 2.4% 82.5%, 2.2% 81.9%, 2% 81.3%, 1.9% 80.6%, 1.7% 79.9%, 1.5% 79.2%, 1.4% 78.5%, 1.2% 77.8%, 1.1% 77%, 1% 76.3%, 0.9% 75.4%, 0.7% 74.6%, 0.6% 73.7%, 0.5% 72.8%, 0.5% 71.8%, 0.4% 70.8%, 0.3% 69.8%, 0.2% 68.7%, 0.2% 67.5%, 0.1% 66.2%, 0.1% 64.8%, 0.1% 63.2%, 0% 61.4%, 0% 59.3%, 0% 56.6%, 0% 50%, 0% 43.4%, 0% 40.7%, 0% 38.6%, 0.1% 36.8%, 0.1% 35.2%, 0.1% 33.8%, 0.2% 32.5%, 0.2% 31.3%, 0.3% 30.2%, 0.4% 29.2%, 0.5% 28.2%, 0.5% 27.2%, 0.6% 26.3%, 0.7% 25.4%, 0.9% 24.6%, 1% 23.7%, 1.1% 23%, 1.2% 22.2%, 1.4% 21.5%, 1.5% 20.8%, 1.7% 20.1%, 1.9% 19.4%, 2% 18.7%, 2.2% 18.1%, 2.4% 17.5%, 2.6% 16.9%, 2.8% 16.3%, 3% 15.7%, 3.2% 15.2%, 3.5% 14.6%, 3.7% 14.1%, 4% 13.6%, 4.2% 13.1%, 4.5% 12.6%, 4.7% 12.1%, 5% 11.7%, 5.3% 11.2%, 5.6% 10.8%, 5.9% 10.3%, 6.2% 9.9%, 6.6% 9.5%, 6.9% 9.1%, 7.2% 8.7%, 7.6% 8.3%, 8% 8%, 8.3% 7.6%, 8.7% 7.2%, 9.1% 6.9%, 9.5% 6.6%, 9.9% 6.2%, 10.3% 5.9%, 10.8% 5.6%, 11.2% 5.3%, 11.7% 5%, 12.1% 4.7%, 12.6% 4.5%, 13.1% 4.2%, 13.6% 4%, 14.1% 3.7%, 14.6% 3.5%, 15.2% 3.2%, 15.7% 3%, 16.3% 2.8%, 16.9% 2.6%, 17.5% 2.4%, 18.1% 2.2%, 18.7% 2%, 19.4% 1.9%, 20.1% 1.7%, 20.8% 1.5%, 21.5% 1.4%, 22.2% 1.2%, 23% 1.1%, 23.7% 1%, 24.6% 0.9%, 25.4% 0.7%, 26.3% 0.6%, 27.2% 0.5%, 28.2% 0.5%, 29.2% 0.4%, 30.2% 0.3%, 31.3% 0.2%, 32.5% 0.2%, 33.8% 0.1%, 35.2% 0.1%, 36.8% 0.1%, 38.6% 0%, 40.7% 0%, 43.4% 0%, 50% 0%, 56.6% 0%, 59.3% 0%, 61.4% 0%, 63.2% 0.1%, 64.8% 0.1%, 66.2% 0.1%, 67.5% 0.2%, 68.7% 0.2%, 69.8% 0.3%, 70.8% 0.4%, 71.8% 0.5%, 72.8% 0.5%, 73.7% 0.6%, 74.6% 0.7%, 75.4% 0.9%, 76.3% 1%, 77% 1.1%, 77.8% 1.2%, 78.5% 1.4%, 79.2% 1.5%, 79.9% 1.7%, 80.6% 1.9%, 81.3% 2%, 81.9% 2.2%, 82.5% 2.4%, 83.1% 2.6%, 83.7% 2.8%, 84.3% 3%, 84.8% 3.2%, 85.4% 3.5%, 85.9% 3.7%, 86.4% 4%, 86.9% 4.2%, 87.4% 4.5%, 87.9% 4.7%, 88.3% 5%, 88.8% 5.3%, 89.2% 5.6%, 89.7% 5.9%, 90.1% 6.2%, 90.5% 6.6%, 90.9% 6.9%, 91.3% 7.2%, 91.7% 7.6%, 92% 8%, 92.4% 8.3%, 92.8% 8.7%, 93.1% 9.1%, 93.4% 9.5%, 93.8% 9.9%, 94.1% 10.3%, 94.4% 10.8%, 94.7% 11.2%, 95% 11.7%, 95.3% 12.1%, 95.5% 12.6%, 95.8% 13.1%, 96% 13.6%, 96.3% 14.1%, 96.5% 14.6%, 96.8% 15.2%, 97% 15.7%, 97.2% 16.3%, 97.4% 16.9%, 97.6% 17.5%, 97.8% 18.1%, 98% 18.7%, 98.1% 19.4%, 98.3% 20.1%, 98.5% 20.8%, 98.6% 21.5%, 98.8% 22.2%, 98.9% 23%, 99% 23.7%, 99.1% 24.6%, 99.3% 25.4%, 99.4% 26.3%, 99.5% 27.2%, 99.5% 28.2%, 99.6% 29.2%, 99.7% 30.2%, 99.8% 31.3%, 99.8% 32.5%, 99.9% 33.8%, 99.9% 35.2%, 99.9% 36.8%, 100% 38.6%, 100% 40.7%, 100% 43.4%

You can now paste that into your CSS to give an element a nice, smooth border radius.

As nice as this is, it’s not good enough. For this to actually be useful, we need to create a LESS mixin so that we can include it in our CSS easily.

If you don’t use LESS, you can probably convert it to SASS or Post-CSS or whatever CSS processor you use without much trouble.

LESS is difficult to work with in cases like this since it lacks official support for loops, functions, and arrays, though the same concepts can be hacked together using recursion and strings (which is what LESS lists are; they are not arrays). As a result, the code ends up being temperamental and ugly. At any rate, here’s what I came up with

.border-squircle(@radius-x; @radius-y) {
.loop(@radius-x; @radius-y; 359; 100% 0);
}
.loop(@radius-x; @radius-y; @counter; @list) when (@counter >= 0) {
@cos-x: cos(unit(@counter, deg));
@x: pow(abs(@cos-x), unit(@radius-x) / 100) * 50 * abs(@cos-x + 0.0000000001) / (@cos-x + 0.0000000001) + 50;

@sin-y: sin(unit(@counter, deg));
@y: pow(abs(@sin-y), unit(@radius-y) / 100) * 50 * abs(@sin-y + 0.0000000001) / (@sin-y + 0.0000000001) + 50;

@percent-x: percentage(round(@x, 1) / 100);
@percent-y: percentage(round(@y, 1) / 100);

@new-list: @percent-x @percent-y, @list;
.loop(@radius-x; @radius-y; (@counter - 1); @new-list);
}
.loop(@radius-x; @radius-y; @counter; @list) when (@counter < 0) {
clip-path: polygon(@list);
}

After playing with it for a bit, I ended up changing how the radius works; now you just supply a percentage, where 0% is a rectangle and 100% is an oval. You must also specify an x radius and a y radius, which gives you finer control over rectangular elements.

As for how the code works, we just iterate from 359 to 0, adding the result of the calculation to a list, then we print it out. There’s a lot of gunk in there to handle units and whatnot, but otherwise it’s pretty straight forward. It also contains the hackiest Math.sign implementation ever:

sign: abs(@cos-x + 0.0000000001) / (@cos-x + 0.0000000001)

This works on the basis of sign(x) = x === 0 ? 0 : |x|/x…except LESS doesn’t have an if statement, so I couldn’t check if x is 0. Instead, I just add 0.0000000001 to x and pray that x is never equal to -0.0000000001.

Finally, you can use the mixin like this:

.border-test {
margin: 2em;
height: 10em;
width: 10em;
background-color: purple;
.border-squircle(50%; 50%);
}

If you’re interested, I set up a JS Bin with this implementation and a couple of examples from earlier in this article.

--

--