Frontend Development 18 min read

How to Build a Circular Countdown Progress Bar with Pure CSS and JavaScript

This step‑by‑step tutorial shows how to create a circular countdown timer by constructing a fixed container, drawing light and dark arcs with CSS, adding a masking circle, and animating the progress using a small JavaScript snippet that updates the rotation and countdown display.

WecTeam
WecTeam
WecTeam
How to Build a Circular Countdown Progress Bar with Pure CSS and JavaScript

Introduction

In a recent project I needed a circular countdown progress bar. This article walks through building it from scratch using only HTML, CSS and a little JavaScript.

final result
final result

Implementation Steps

Add Container

Make the outer container fixed so it can be placed anywhere on the page.

<code>&lt;div class="task-container"&gt;&lt;/div&gt;</code>

Corresponding CSS:

<code>.task-container {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    width: 65px;
    height: 65px;
    display: flex;
    justify-content: center;
    align-items: center;
}</code>

Draw Base Circle

Add a concentric circle to serve as the background of the timer.

<code>&lt;div class="task-container"&gt;
    &lt;div class="task-cicle"&gt;&lt;/div&gt;
&lt;/div&gt;</code>
<code>.task-container {
    /* same styles as above */
}
.task-cicle {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 53px;
    height: 53px;
    border-radius: 50%;
    background: #FFFFFF;
    box-shadow: 0px 0px 12px 0px rgba(0,0,0,0.05);
}</code>
base circle
base circle

Draw Right Arc

Use a right‑half rectangle and set only the top and right borders to create the right arc.

<code>&lt;div class="task-container"&gt;
    &lt;div class="task-cicle"&gt;
        &lt;div class="task-inner"&gt;
            &lt;div class="right-cicle"&gt;
                &lt;div class="cicle-progress cicle1-inner"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;</code>
<code>.right-cicle {
    width: 23px;
    height: 46px;
    position: absolute;
    top: 0;
    right: 0;
    overflow: hidden;
}
.cicle-progress {
    position: absolute;
    top: 0;
    width: 46px;
    height: 46px;
    border: 3px solid transparent;
    box-sizing: border-box;
    border-radius: 50%;
}
.cicle1-inner {
    left: -23px;
    border-right: 3px solid #e0e0e0;
    border-top: 3px solid #e0e0e0;
    transform: rotate(-15deg);
}</code>
right arc
right arc

Draw Left Arc

Apply the same principle to the left side, setting only the top and left borders.

<code>&lt;div class="task-container"&gt;
    &lt;div class="task-cicle"&gt;
        &lt;div class="task-inner"&gt;
            ...
            &lt;div class="left-cicle"&gt;
                &lt;div class="cicle-progress cicle2-inner"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;</code>
<code>.left-cicle {
    width: 23px;
    height: 46px;
    position: absolute;
    top: 0;
    left: 0;
    overflow: hidden;
}
.cicle2-inner {
    left: 0;
    border-left: 3px solid #e0e0e0;
    border-top: 3px solid #e0e0e0;
    transform: rotate(15deg);
}</code>
left arc
left arc

Draw Right Progress Bar

Set the right‑half progress bar with a bright border and rotate it from -135° to -15°.

<code>.cicle3-inner {
    left: -23px;
    border-right: 3px solid #feca02;
    border-top: 3px solid #feca02;
    transform: rotate(-135deg);
}</code>
right progress
right progress

Draw Left Progress Bar

Set the left‑half progress bar with a bright border and rotate it from 195° to 315°.

<code>.cicle4-inner {
    left: 0;
    border-left: 3px solid #feca02;
    border-top: 3px solid #feca02;
    transform: rotate(195deg);
}</code>
left progress
left progress

Add Masking Circle

Place a larger concentric circle to hide the tiny tail that appears after clipping.

<code>.mask-inner {
    position: absolute;
    left: 0;
    top: 0;
    width: 39px;
    height: 39px;
    border: 4px solid transparent;
    border-radius: 50%;
    border-left: 4px solid #FFFFFF;
    border-top: 4px solid #FFFFFF;
    transform: rotate(195deg);
}</code>
mask circle
mask circle

JavaScript Animation

The script calculates the rotation per second, creates keyframe rules on the fly, and updates the countdown text. Clicking the timer pauses or resumes the animation.

<code>const rightCicle = document.getElementById('rightCicle');
const leftCicle = document.getElementById('leftCicle');
const timeDom = document.getElementById('time');
let isStop = false;
let timer;
const totalTime = 10; // total seconds
const halfTime = totalTime / 2;
const initRightDeg = -135;
const initLeftDeg = 195;
const halfCicle = 120; // degrees each side rotates
const perDeg = 120 / halfTime; // degrees per second
let inittime = 10;
let begTime;
let stopTime;
function run() {
    const time = inittime;
    let animation;
    if (time > halfTime) {
        animation = `
@keyframes task-left {
    0% { transform: rotate(${initLeftDeg + (totalTime - time) * perDeg}deg); }
    100% { transform: rotate(${initLeftDeg + halfCicle}deg); }
}
.task-left { animation-name: task-left; animation-duration: ${time - halfTime}s; animation-timing-function: linear; animation-fill-mode: forwards; }
@keyframes task-right {
    0% { transform: rotate(${initRightDeg}deg); }
    100% { transform: rotate(${initRightDeg + halfCicle}deg); }
}
.task-right { animation-name: task-right; animation-duration: ${halfTime}s; animation-timing-function: linear; animation-delay: ${time - halfTime}s; animation-fill-mode: forwards; }
`;
    } else {
        animation = `
@keyframes task-left {
    0% { transform: rotate(${initLeftDeg + halfCicle}deg); }
    100% { transform: rotate(${initLeftDeg + halfCicle}deg); }
}
.task-left { animation-name: task-left; animation-duration: 0s; animation-fill-mode: forwards; }
@keyframes task-right {
    0% { transform: rotate(${initRightDeg + (halfTime - time) * perDeg}deg); }
    100% { transform: rotate(${initRightDeg + halfCicle}deg); }
}
.task-right { animation-name: task-right; animation-duration: ${time}s; animation-timing-function: linear; animation-fill-mode: forwards; }
`;
    }
    animation += `.stop { animation-play-state: paused; } .run { animation-play-state: running; }`;
    const styleDom = document.createElement('style');
    styleDom.type = 'text/css';
    styleDom.innerHTML = animation;
    document.getElementsByTagName('head')[0].appendChild(styleDom);
    leftCicle.classList.add('task-left');
    rightCicle.classList.add('task-right');
    begTime = Date.now();
    countDown();
}
function countDown() {
    if (begTime && stopTime) {
        const runtime = stopTime - begTime;
        if (runtime % 1000 > 500) { inittime -= 1; }
    }
    begTime = Date.now();
    timeDom.innerText = `${inittime}秒后获得 `;
    timer = setInterval(() => {
        inittime -= 1;
        timeDom.innerText = `${inittime}秒后获得 `;
        if (inittime <= 0) clearInterval(timer);
    }, 1000);
}
timeDom.addEventListener('click', () => {
    if (isStop) {
        isStop = false;
        countDown();
        leftCicle.classList.remove('stop'); leftCicle.classList.add('run');
        rightCicle.classList.remove('stop'); rightCicle.classList.add('run');
    } else {
        stopTime = Date.now();
        isStop = true;
        clearInterval(timer);
        leftCicle.classList.remove('run'); leftCicle.classList.add('stop');
        rightCicle.classList.remove('run'); rightCicle.classList.add('stop');
    }
}, false);
run();
</code>

Conclusion

The light arc and bright progress bar involve several layers of clipped circles. By removing clipping temporarily you can see each layer's shape, which helps understand the construction. The final result is a smooth, animated circular countdown timer.

final timer
final timer
frontendJavaScriptCSSHTMLProgress BarCountdown
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.