I Just Wanted a Semicircle, CSS

I Just Wanted a Semicircle, CSS

I was building a small tag component and wanted a shape that seemed almost too simple to think about: slightly rounded corners on the left, and a perfect semicircle on the right. Just like an old-school pricing tag. Like this:

The word “Agreements” surrounded by an uneven rounded outline

At a fixed height of say 24px, this is easy:

.tag {
	border-radius: 6px 12px 12px 6px;
}

Done. Except the height of my tag is dynamic, so I changed 12px to 50% and thought I could call it a day. Wrong!

.tag {
	border-radius: 6px 50% 50% 6px;
}

What I actually want in plain English is:

The radius of the two right corners should be half the height of the element.

If the element is 24px high, use 12px. If it’s 32px high, use 16px, and so on. But that’s not what 50% means in border-radius.

Percentage values for the horizontal radius are relative to the width of the element, while percentages for the vertical radius are relative to its height.

So 50% doesn’t give me a circle based on the element’s height. On a tag that’s much wider than it is tall, it gives me a very stretched ellipse.

The word “Agreements” surrounded by an uneven rounded outline

That looks terrible. And the slash syntax doesn’t help either:

border-radius: 6px 50% 50% 6px / 6px 50% 50% 6px;

The horizontal percentages are still based on the width.

What I need is essentially this imaginary CSS:

border-radius: 6px calc(50% of height) calc(50% of height) 6px;

As far as I can tell, CSS has no way of expressing that directly.

But there’s a common CSS trick for making pill-shaped elements: just use an absurdly large radius. Surely that would work for this case too?

.tag {
	border-radius: 6px 999px 999px 6px;
}

Nope, something strange happens here: the 6px corners on the left almost completely disappeared.

But this is actually defined behavior, although it may not be what you expect.

When the specified corner radii are too large to fit inside the box, CSS scales all of the radii down proportionally until they fit.

For example, on a 24px high element:

border-radius: 6px 50px 50px 6px;

asks for 50px + 50px of vertical radius on the right side. There’s only 24px available, so the radii need to be scaled by:

24 / 100 = 0.24

And that scaling factor applies to every corner, not just the oversized ones. So the effective radii become approximately:

1.44px 12px 12px 1.44px

The right side becomes the semicircle I wanted, but my carefully specified 6px corners on the left get shrunk to almost nothing.

The word “Agreements” surrounded by an uneven rounded outline

So the classic 999px trick doesn’t work here either. The larger I make the right-hand radius, the smaller CSS makes the left-hand one.

Which is both perfectly logical once you know the rule and extremely confusing until you do.

I also tried the new shape() function, but I couldn’t get it to do exactly what I wanted either. This may be worth exploring further though.

The solution I ended up with was to stop trying to make the tag itself do everything.

Instead, the tag gets its normal left-hand corners and no right-hand border:

.tag {
	--border-color: pink;
	position: relative;
	border: 1px solid var(--border-color);
	border-inline-end: 0;
	border-radius: 6px 0 0 6px;
}

Then I add the rounded end as a pseudo-element:

.tag::after {
	content: '';
	position: absolute;
	inset-block: -1px;
	inset-inline-start: 100%;
	aspect-ratio: 1 / 2;
	border: 1px solid var(--border-color);
	border-inline-start: 0;
	border-radius: 0 999px 999px 0;
}

The important part is this:

aspect-ratio: 1 / 2;

The pseudo-element is always twice as tall as it is wide.

That means if the tag is 24px high, the cap is 12px wide. If the tag grows to 32px, the cap becomes 16px wide.

In other words, instead of somehow convincing border-radius to use half the element’s height as its radius, I’m creating an element whose width is half its height.

The absurdly large radius can then happily turn that element into a semicircle.

That’s it. This works. But I’m still a bit bummed that I can’t achieve this by simply using border-radius on a single element. There’s nothing inherently wrong with pseudo-elements, but introducing an extra box just to draw half a circle certainly feels like a workaround for something that should be much simpler.