惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cloudbric
Cloudbric
T
Threat Research - Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
P
Privacy & Cybersecurity Law Blog
H
Help Net Security
云风的 BLOG
云风的 BLOG
G
GRAHAM CLULEY
Spread Privacy
Spread Privacy
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
Arctic Wolf
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
P
Privacy International News Feed
Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
M
MIT News - Artificial intelligence
The Register - Security
The Register - Security
Recorded Future
Recorded Future
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
A
About on SuperTechFans
W
WeLiveSecurity
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
The GitHub Blog
The GitHub Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Check Point Blog
Y
Y Combinator Blog
月光博客
月光博客
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
U
Unit 42
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threatpost
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google Online Security Blog
Google Online Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
Hugging Face - Blog
Hugging Face - Blog
MongoDB | Blog
MongoDB | Blog
博客园 - 司徒正美

Ben Frain

So, you want a React modal that uses the <dialog> element and transitions in AND out? Scroll indicators on tables with background colours using animation-timeline Review: SoundPEATS Clip1 Open ear clip-on headphones VS Code – highlight just the active indent guide Review: MoErgo Go60, a split ergonomic and fully programmable keyboard Review: Kinesis mWave mechanical ergonomic and programmable keyboard iOS26 Safari theme-color/tab-tinting with fixed position elements is a mess New Book: Responsive Web Design with HTML5 and CSS, 5th Edition Use @supports with a proxy feature/value for features you can’t test for (@starting-style) First adventures in View Transitions Review: Benq Screenbar Pro and Halo lightbars. The kit you never knew you needed! Center items in a container, and make then left aligned when they overflow Review: Open Ear Headphones – Bose Open Ultra v Huawei FreeClip In search of the perfect autocomplete for CSS Managing multiple versions of node, without NVM or additional tools Review: Keychron Q14 Max Alice 96 Key mechanical keyboard NEW VIDEO COURSE: Responsive Web Design with HTML5 and CSS Is CSS Grid really slower than Flexbox? Review: Advantage360 Pro Signature Edition 2024 mechanical ergonomic keyboard More Keys or Fewer Keys for mechanical keyboards Yes! You can use position: sticky and overflow together Neovim – how to do project-wide find and replace? Review: Keyboardio Model 100, split, wooden, mechanical keyboard Struggling to learn SwiftUI How to create rounded gradient borders with any background in CSS How to get equal size icons in the cmp completion menu of Neovim with Kitty terminal Review: Dygma Defy, split, mechanical, programmable ergonomic keyboard What’s the best way to reset WAAPI chained animations? Using CSS @property inside shadowRoot (web components) workaround Dynamically create a ref for items when iterating over them in lit.dev templates Selecting and pausing running animations in Lit Web Components New Web APIs — a popover on top of a dialog element can’t be interacted with? Review: ZSA Voyager, split, mechanical keyboard Russel Brand, narcissism, and a sadly common pattern… When it comes to text editors, I feel like Goldilocks Simple settings for writing and converting markdown with Sublime Text Review: The ZSA Platform tenting kit for the Moonlander keyboard Logitech MX Master 3/3s scroll wheel fix Building a line graph with CSS clip-mask Review: Dell 6K 32″ Monitor U3224KBA I broke my keyboard! Swapping the key switches in the Kinesis Advantage360 Pro HUGE macOS Productivity boost: Set-up simple, keyboard only, instant App switching and arrangement Adding to $PATH for a central location for Neovim/NPM tools Neovim Power Tips: Volume 2 Review: MoErgo Glove80, split, wireless, columnar ergonomic keyboard with RGB Review: Kinesis Advantage 360 Pro — split ergo mechanical keyboard Review: Dactyl Manuform – an ergonomic, custom built mechanical keyboard How to animate along an SVG path at the same time the path animates? Getting the context of Web Components (lit)
A single element CSS donut timer/countdown timer, that can sit on any background
Ben Frain · 2025-01-08 · via Ben Frain

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;
}

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.