Semicircular Borders

Ever since border-radius appeared in the CSS toolkit, people have been making circular or rounded elements. They're great for a variety of reasons, but I find myself reaching for them a lot when making buttons and small, sticker-like labels. In order to help them stand-out from the background, I often set a border around them; the fact that you can do this in a couple of lines of code and get nicely rounded edges remains a bit magical to me. (Yes, I do remember the days of carefully positioning images at each corner of an element to fake this look, how can you tell? 😅)

Recently, I was noodling on a design that used one of these circular elements as a divider between blocks of content. You'd get a paragraph or two of text, and then a circular label that indicated a subtle change in context. In this case, the content was appearing in chronological order, and the circular element was stating division in time; something that you might see, for instance, on a conference schedule page, separating different speakers.

In and of itself, this would be a simple enough piece of content to implement, probably being a single <time> element, maybe with an <hr> or similar to mark the division. However, this particular design called for only the lower half of the circle to have a border. It looked a little like this:

The text "12:00", meaning noon, within a white circle. The bottom half of the circle has a thick, purple border, making it look like the time is sitting within a bowl.

Making it circular and setting the relevant fonts, background colours, etc. are all easy:

time {
	/* Make it circular */
	width: max-content;
	aspect-ratio: 1 / 1;
	
	/* Style the container */
	padding: 1rem;
	background: white;

	/* Center the text */
	display: flex;
	place-items: center;
}

But what about that border?

I figured that I had two good options[1]:

  1. Use a pseudo-element to draw a semicircle and then absolutely position it behind the <time> element itself;
  2. Manipulate a border to somehow only appear on one half of the element.

For a few factors not worth mentioning here, the pseudo-element option was quickly taken off the table, so that left me with a border.

Border Control

You may be thinking that using a border is clearly the obvious solution to begin with, but the reality isn't as simple as it first appears. The ideal option would involve setting border-bottom (or, preferably, border-block-end) and having this magically just work. However, if you try it out, you'll see that the result isn't exactly fit for purpose:

The same circular time element, but now with a purple, crescent-moon shape over the bottom edge.

It's certainly an interesting effect, and it is putting a border across the lower half of the circle, but it doesn't exactly match the design. Perhaps if we add a border to one of the sides as well[2] 🤔 

The time element with over two-thirds now covered in a sickle-shaped purple faux border.

Nope, that's worse! 😂 To understand what's happening here, it's perhaps simplest to remove the border-radius for a second. 

A white square with the text "12:00" and the left and bottom borders with a thick, purple border.

That should hopefully make it clear that a simple border attribute is never going to do what we want. Once you start rounding the edges of an element, borders have to try and preserve the resultant arc, but they cannot know that you've fundamentally changed the shape (in this instance, to a circle), so they still try to play by the rules of rectangles and polygons. The result are borders that effectively curve to infinity. If we want a hard edge within a circular border, we're going to have to approach this differently.

A Difficulty Gradient

Gradients have unlocked a lot of design opportunities within CSS. Creating a "hard stop" gradient is fairly trivial: all you need are two colours and a linear gradient that switches them at the 50% mark, e.g:

linear-gradient(red 0% 50%, blue 50% 100%);

For our purpose, we don't want to have two colours per se, but rather one colour and one, well, lack of colour. Thankfully CSS includes the ever-useful transparent within its valid colour options, so we can use the same logic on a linear gradient which only has one "true" colour ‒ in this case: rebeccapurple ‒ and pairs it with, well, nothing. We can even define the direction, forcing the gradient to render top-to-bottom, which results in the following statement:

linear-gradient(to bottom, transparent 0% 50%, rebeccapurple 50% 100%);

Unfortunately, the border-color attribute in CSS does not accept gradients as an option... but the background-image attribute does! And what's more, it accepts multiple gradients, which can then be layered 🎉

First, we'll remove the existing background-color statement and replace it with two new options: a transparent border that will define the width and style of our final design; and a background-image with two gradients, one to control the colour of our content area and the other that will, eventually, control our border colour:

time {
	...
	
	/* Style the border */
	border: 0.5em solid transparent;
	border-image: 
		linear-gradient(white, white),
		linear-gradient(to bottom, transparent 0% 50%, rebeccapurple 50% 100%);
}

Right now that doesn't look like much ‒ all it really seems to do is make the circle larger and give it some odd antialiasing artefacts around the edges (at least, it does in Firefox on Windows) 😅 But under the hood, this has set the stage very nicely.

What we're actually seeing here is that the two gradients are being applied to the entire element, with the first gradient layering above the second one. Because that initial gradient is just fully white, and because our border is transparent, both the border and the content area appear as solid white (ignoring the clipping around the edges). Thankfully, we have a surprising amount of control as to how these gradients are rendered, allowing us to modify the default behaviour significantly.

First, we're going to use background-clip to change which parts of the element each gradient can be applied to. By default, this uses the value border-box, allowing it to set the background across entire elements, borders and all. We only want that first, white gradient to apply within the element, so we can change the clip value to padding-box, which restricts it to the content area of the element, plus any padding. The second gradient can be left with the value of border-box, so that it can expand into our transparent border:

background-clip: padding-box, border-box;

That has an immediate impact, though it definitely isn't exactly the desired outcome:

A white circle with "12:00" inside. Balanced on the top is small sliver of a purple circle, whilst around the bottom is part of a semicircle with the lower edge missing.
What on earth... 😅

We've successfully restricted the white gradient to only fill the content area ‒ effectively mimicking a regular ol' background-color in the process ‒ but what's going on with that border? We're now seeing our purple/transparent gradient applied to our outer edge, but for some reason it's "wrapping" vertically, rather than neatly starting at the top of the element.

To change that, let's take a look at background-origin. This property lets us define where a background should originate. The problem we're running into is that, by default, the value here is set to padding-box. So whilst we've specified for our second gradient to render to bottom (meaning that it should start at the top of the element), the background-origin is setting that top point at the edge of the padding area. That explains why the gradient is cut off at the bottom, and the reason it appears to "wrap" onto the top is that it's trying to repeat to fill the available space. So, if we change background-origin to a value of border-box, then it will begin to render the gradient from the top-edge of the border all the way down to the bottom-edge, giving us the semicircle of purple that we've been aiming for:

A white circle with "12:00" inside, the bottom half with a thick, purple border.
🎉🎉🎉

Making it Modular

I'm pretty happy with how this solution all came together. It builds on some of the work I've done in the past getting rounded borders on <input> elements, and has really helped me understand some of the more esoteric background and border options that we now have available within CSS. But before I sign off, I wanted to create a single block of code that could be reused anywhere else that I wanted to apply a similar effect. I've augmented this with some Custom Properties to make it a bit more flexible and customisable.

Critically, this lets you define which side of the circle you want the border to be placed on, which I think is kinda neat 😅

So, without any more gilding of the lily, here is that code (or you can check it out on CodePen):

.semicircle-border {
	--thickness: 0.5em;
	--border-color: orange;
  	--background-color: slategray;
  	--font-color: white;
  	--semicircle-side: left;
  
  	/*  Border  */
  	border: var(--thickness) solid transparent;
  	background-image: linear-gradient(var(--background-color), var(--background-color)), linear-gradient(to var(--semicircle-side), transparent 0% 50%, var(--border-color) 50% 100%);
  	background-origin: border-box;
  	background-clip: padding-box, border-box;
  	border-radius: 50%;

  	/*  General styling  */
  	display: flex;
  	place-items: center;
  	width: max-content;
  	aspect-ratio: 1 / 1;
  	color: var(--font-color);
  	padding: 1rem;
}

Explore Other Articles

Older

Crafty Field Migration

Migrating content between fields in Craft is not as simple as it might be. In the wake of Craft 5, I've been getting a lot of practice, and wanted to write up some of the techniques I've been using.

Conversation

Want to take part?

Comments are powered by Webmentions; if you know what that means, do your thing 👍

Footnotes

  • <p>Creating a border along half of a circular element using CSS gradients and background images.</p>
  • Murray Champernowne.
Article permalink