I’d wager that most times we’re rounding box corners in CSS, we’re applying a uniform border-radius value across the border. It’s a nice touch of polish in many designs. But there are times when we might want different radii for different corners. Easy, right? That way the property takes four values. Well, as it turns out, it’s actually possible to paint ourselves into a corner because rounded borders are capable of overlapping one other.
Many of us know the common “999em hack” for getting a “pill-shaped” rectangle:
We set the border-radius
to an absurdly large number, like 999em or 999vmax, and instead of becoming some kind of impossible, Escher-esque möbius strip, the corners are nicely rounded off to form a semicircle. This is convenient because it means we don’t have to know the dimensions of the rectangle to achieve this effect — it “just works.”
The origins of this trick are murky to me, but I found an early example in a comment on Lea Verou’s blog by David Baron.
But, like many “hacks”, we can encounter some odd behavior in certain edge cases. For instance, why does that work when this doesn’t:
We want the right side of the rectangle to be “pill-shaped,” and the left side to have corners rounded to 40px. But our 40px corners are gone! Where did they go?
The answer is that they didn’t go anywhere; the browser has just reduced their values so close to zero that they merely look like they’re gone.
The browser is diverging somehow from the values we requested, but when and how does it decide to step in? Let’s check the spec:
Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f.
Ah, that explains it! Have a great rest of your week! :wipes hands conclusively:
…I’m joking, of course. That requires a little decoding, so let’s look at it two ways, mathematically and geometrically. Always keep in mind that the purpose of this formula is to prevent radius overlap. In fact, that’s why the “999em hack” works in the first place!
Here’s what I mean.
In plain English, the browser is essentially thinking: “Shrink all radii proportionally until there is no overlap between them.” (Note that it’s the radii that mustn’t overlap; the circles they form may indeed overlap.)
But a computer doesn’t understand English, so what the formula does is this.
First, it calculates the ratio of the length of each side of the rectangle to the sum of the radii that touch it. So, in our standard “pill hack” that works out to:
[Width of Side] / [Adjacent Border Radius 1 + Adjacent Border Radius 2]
Top: 200px / (400px + 400px) = 0.25
Right: 100px / (400px + 400px) = 0.125
Bottom: 200px / (400px + 400px) = 0.25
Left: 100px / (400px + 400px) = 0.125
Then it multiplies all of the radii by the smallest of these ratios. The smallest ratio is 0.125, so we’ll multiply that by our initial 400px radii:
400px * 0.125 = 50px
That leaves all our radii at 50px. For a rectangle whose shortest sides are 100px, this gives us a perfect pill shape. Cool! Take a look at the following animation:
(To make things easier to see, we’re using 400px here for our “absurdly large” radii rather than 999em; they’ll overlap as long as they are at least half the length of the shortest side of the rectangle.)
The circles representing the radii we specified start out at their requested size, then shrink by the ratio dictated by the formula above. What’s important to note is that they all shrink by the same ratio. It’s perhaps more intuitive here, since they all start out at the same size anyway.
Now let’s go back to our “broken” example that got everything started.
What’s going on here? Let’s try a less extreme example to show border radii that are affected by this shrinking, but not to the extent that they virtually disappear:
We can see there that we’re not getting the 40px radii we’re asking for in the top-left and bottom-left corners, but we are getting something. Let’s work through that formula again, first finding the ratios between all the sides and their adjacent radii:
Top: 200px / (40px + 400px) = 0.455
Right: 100px / (400px + 400px) = 0.125
Bottom: 200px / (40px + 400px) = 0.455
Left: 100px / (40px + 40px) = 1.25
Again, our lowest ratio there is 0.125, so we multiply all the specified radii by that amount, giving us 50px radii for the right corners and 5px radii for the left corners.
What the formula is ensuring here is that the two large radii on the right side of the rectangle don’t overlap, but in doing so, has shrunk the small radii on the left side of the rectangle more than they “need to be” shrunk to prevent radius overlap on the top, bottom, and left sides.
Here’s a richer example that shows what happens in different circumstances. Play around with some of the values to see what happens. Again, the large sizes are the radii we specified in code, and the small sizes are how the browser reconciles them to prevent overlap.
Why would the Oz-like spec makers decide on doing things this way? Why not shrink the larger border radii first, rather than shrinking all radii from the start?
I can’t read their minds, of course, but the benefit of this approach is that the radii maintain their proportions to one another. If we were to instruct the browser to decrease the largest radius until there was no overlap or until it was equal to the second largest radii (whichever came first) and repeat, then our “hybrid pill hack” would have worked; but there are cases where you could end up with four equal radii when the user had asked for very different sizes. In other words, the implementation has to be “unfaithful” to the numbers one way or the other, and this is the way they chose (wisely, I think).
Thanks to my colleagues Catherine for first noticing this “disappearing radii” issue and James for helping me understand the spec!
I really like the formula thing. I knew the shrink behaviour from Lea’s talk https://youtu.be/b9HGzJIcfDE But was not aware that there is a formula. Is that formula a part of spec or it is still in browser vendors to decide?
I made this border-radius tool where you can move all eight handles individually. Maybe this can help to understand what happens when you have two sides that add up to more than 100%. Back in the day, I did not understand it, even though I made this tool :-) So thanks a lot for this article.
https://9elements.github.io/fancy-border-radius/full-control.html#41.38.37.34-68.49.51.64-.
I really like you tool. Thank you!
I’ve never heard of the 999em hack. It seems easier to set the border-radius to understandable values. For example, to create a “pill” button:
border-radius: calc(var(--button-height) / 2) / 50%;
I agree. never heard of it and if I stumble across it, it would probably confuse me …
css provides variables and functions, which in my opinion are nicer to read and not as much as a hack:
Thanks! Your solution works great if the size of the element is predictable; when its size isn’t predictable (for instance, if it’s going to have an unknown amount of text inside of it), you can’t specify a fixed value for the border radius. Similarly, if you’re using a design system where, for instance, the padding might change, or a font size might change, using a fixed value for the border radius can result in some unpredictable behavior. The 999em hack ensures that your element will always have pill-style corners.
I always use
border-radius: 100vh
. That way it looks kind of like 100% which makes it easier to remember.Really great article, but I would suggest placing this part:
“(To make things easier to see, we’re using 400px here for our “absurdly large” radii rather than 999em; they’ll overlap as long as they are at least half the length of the shortest side of the rectangle.)”
A bit higher in the explanation, as I spend way too long trying to figure out where you were getting the 400px from in the first example :P