During last year, I championed how you can use rounded gradient borders with any background. This approach has solved a great many UI challenges for me. Today, I want to document how it can solve another.

What if you want a countdown ‘donut timer’, the sort of UI used throughout iOS to indicate a download occurring. I have made these on the web before using JS and SVGs. It’s a perfectly fine approach, but where a CSS only approach exists for something purely presentational and fairly simple, and by simple I mean not a bunch of chained animations, where WAAPI makes more sense, I generally like to use CSS.

Here it is:

See the Pen donut timer on any background by Ben Frain (@benfrain) on CodePen.

Making the donut

This is purely presentational so markup wise I’m going with this:

<span class="donut"></span>

And I’m styling the pie timer like this.

.donut {
    display: block;
    height: 40px;
    width: 40px;
    border-radius: 50%;
    border: 5px solid transparent;
    background: conic-gradient(
            green 0,
            green var(--countdown),
            #a7a7a7 var(--countdown)
        )
        border-box;
    mask:
        linear-gradient(#fff 0 0) padding-box,
        linear-gradient(#fff 0 0);
    mask-composite: exclude;
    animation: deplete var(--countdownstart) linear forwards infinite;
}
If you are targeting older browsers, be aware there are -webkit- variants of mask-composite with slightly different syntax but that still achieve the same result. You can find them in my earlier post.

There are two main things here. How we ‘empty’ the circle over time, and how we get the ‘punched out’ donut/flat torus shape with no inner, so this shape can sit on any background and the background will show through the gap.

Emptying the donut with conic-gradient

Consider the changing color of the element to show empty/full first. The fill/empty is achieved by using a conic-gradient. We use a custom property as the placeholder for where the line between filled and empty is. We set the same color at the beginning and the variable position of both the filled and empty colors. As this variable changes over duration, it creates the illusion of the donut emptying. The first two color stops of the gradient are the ‘fill’ and the last one is the ‘background’.

Without any mask, the donut would merely be a round element, filled with a conic-gradient background; a filled donut at this point rather than a ring donut. Nothing wrong with that, I like a filled donut as much as the next guy, but as it is January and I’ve over-indulged in the Christmas festivities, I want to skip the middle.

Cutting out the middle of the circle with a mask

Lets cut the middle out to make our real donut shape.

We are setting a 5px border on this element, and that will be the width of our donut timer. This can be set to whatever width suits. It also does not need to be round, you can remove the border radius and all will still be good (but rectangular).

We will cut the middle out using a mask. A mask needs to be an image, and rather than make one in a graphics application, we can make an image in CSS with the linear gradient function. But the real magic is that we are making our image from the composite of two images. The first linear gradient has a padding-box so as to not include the border-area, and the second does not. Then the mask-composite: exclude excludes one from the other, leaving the donut shape. That donut shape then masks our conic-gradient, only allowing the border area to show. Perfect.

Animating the countdown

Now, we also need the animation to visually change the position of the gradient stops to make the countdown work and change from one color to another creating the effect of the donut reducing:

We will create some keyframes. And at 100% we want to set our --countdown Custom Property to be 0%.

@keyframes deplete {
    100% {
        --countdown: 0%;
    }
}

Now, ordinarily, that would make no sense to the browser, so we can use @property to tell the browser how we want that --countdown Custom Property to behave.

@property --countdown {
    syntax: "<percentage>";
    initial-value: 100%;
    inherits: false;
}

So this says --countdown is a percentage, that starts at 100% and does not inherit.

Putting it all together

We have a background where the color stops will animate. We have our shape thanks to the mask, all that remains is to decide the duration for the animation to run over. You could hard-code this for your needs (the example uses 10s and repeats), or send it in with another Custom Property.

I find this a very clean implementation. It might not look like much but this is a very flexible approach. It can sit on any background, be any size, the gradient of the fill can be any combination of colors – I used solid colors but you could do whatever.