TikTok Tutorial #43 - How to create a Download Progress Circle Animation in CSS & Javascript

Learn with us how to create a Download Progress Circle Animation in CSS & Javascript!

If you found us on TikTok on the following post, check out this article and copy-paste the full code!

Happy coding! 😻

@creative.tim For the full code check TikTok Tutorial #43 😇 Link in bio ✌️ #programmingexercises #webdevelopment #csscoding #javascript ♬ original sound - Creative Tim

Contents:
1. HTML Code
2. CSS Code
3. Javascript Code

Get your code ⬇️

1. HTML Code

<button class="button">
    <svg class="circle" viewBox="0 0 76 76">
        <circle class="default" cx="38" cy="38" r="36"></circle>
        <circle class="active" cx="38" cy="38" r="36"></circle>
    </svg>
    <div class="icon">
        <svg class="line" viewBox="0 0 4 37">
            <line x1="2" y1="2" x2="2" y2="35"></line>
        </svg>
        <div>
            <svg class="arrow" viewBox="0 0 40 32"></svg>
            <svg class="progress" viewBox="0 0 444 10">
                <path d="M2,5 L42,5 C60.0089086,6.33131695 73.3422419,6.99798362 82,7 C87.572404,7.00129781 91.0932494,1.72677301 102,1.99944178 C112.906751,2.27211054 112.000464,7.99986045 122,8 C131.999536,8.00013955 132,2 142,2 C152,2 152,8 162,8 C172,8 172,2 182,2 C192,2 192,8 202,8 C212,8 212,2 222,2 C232,2 232,8 242,8 C252,8 252,2 262,2 C272,2 272,8 282,8 C292,8 292,2 302,2 C312,2 312,8 322,8 C332,8 332,2 342,2 C352,2 351.897852,7.49489262 362,8 C372.102148,8.50510738 378.620177,5.22532154 402,5 L442,5"></path>
            </svg>
        </div>
    </div>
    <span>0%</span>
</button>

<!-- twitter / dribbble -->
<a class="dribbble" href="https://dribbble.com/shots/9683055-Download-animation" target="_blank"><img src="https://dribbble.com/assets/logo-small-2x-9fe74d2ad7b25fba0f50168523c15fda4c35534f9ea0b1011179275383035439.png" alt=""></a>
<a class="twitter" target="_top" href="https://twitter.com/aaroniker_me"><svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72"><path d="M67.812 16.141a26.246 26.246 0 0 1-7.519 2.06 13.134 13.134 0 0 0 5.756-7.244 26.127 26.127 0 0 1-8.313 3.176A13.075 13.075 0 0 0 48.182 10c-7.229 0-13.092 5.861-13.092 13.093 0 1.026.118 2.021.338 2.981-10.885-.548-20.528-5.757-26.987-13.679a13.048 13.048 0 0 0-1.771 6.581c0 4.542 2.312 8.551 5.824 10.898a13.048 13.048 0 0 1-5.93-1.638c-.002.055-.002.11-.002.162 0 6.345 4.513 11.638 10.504 12.84a13.177 13.177 0 0 1-3.449.457c-.846 0-1.667-.078-2.465-.231 1.667 5.2 6.499 8.986 12.23 9.09a26.276 26.276 0 0 1-16.26 5.606A26.21 26.21 0 0 1 4 55.976a37.036 37.036 0 0 0 20.067 5.882c24.083 0 37.251-19.949 37.251-37.249 0-.566-.014-1.134-.039-1.694a26.597 26.597 0 0 0 6.533-6.774z"></path></svg></a>

2. CSS Code

.button {
    --default: #D5A021;
    --active: #fff;
    position: relative;
    border: none;
    outline: none;
    background: none;
    padding: 0;
    -webkit-appearance: none;
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;
    transform: scale(var(--s, 1));
    transition: transform .2s;
    &:active {
        --s: .96;
    }
    svg {
        display: block;
        fill: none;
        stroke-width: var(--sw, 3px);
        stroke-linecap: round;
        stroke-linejoin: round;
    }
    .circle {
        width: 76px;
        height: 76px;
        transform: rotate(-90deg);
        circle {
            &.default {
                stroke: var(--default);
            }
            &.active {
                stroke: var(--active);
                stroke-dasharray: 227px;
                stroke-dashoffset: var(--active-offset, 227px);
                transition: stroke-dashoffset var(--all-transition, 4s) ease var(--all-delay, .8s);
            }
        }
    }
    span {
        display: block;
        position: absolute;
        left: 0;
        right: 0;
        text-align: center;
        bottom: 13px;
        font-weight: 500;
        font-size: 10px;
        color: var(--active);
        opacity: var(--count-opacity, 0);
        transform: translateY(var(--count-y, 4px));
        animation: var(--count, none) .3s ease forwards var(--all-delay, 4.6s);
        transition: opacity .2s ease .6s, transform .3s ease .6s;
    }
    .icon {
        --sw: 2px;
        width: 24px;
        height: 40px;
        position: absolute;
        left: 50%;
        top: 50%;
        margin: -20px 0 0 -12px;
        svg {
            &.line {
                width: 4px;
                height: 37px;
                stroke: var(--active);
                position: absolute;
                left: 10px;
                top: 0;
                stroke-dasharray: 0 33px var(--line-array, 33px) 66px;
                stroke-dashoffset: var(--line-offset, 33px);
                transform: translateY(var(--line-y, 0));
                opacity: var(--line-opacity, 1);
                transition: stroke-dasharray .2s, stroke-dashoffset .2s, transform .32s ease var(--all-delay, .25s);
            }
        }
        div {
            width: 40px;
            height: 32px;
            position: absolute;
            overflow: hidden;
            left: 50%;
            bottom: 1px;
            margin-left: -20px;
            transform: translate(var(--icon-x, 0), var(--icon-y, 0));
            transition: transform .3s ease var(--all-delay, 4.8s);
            animation: var(--overflow, none) 0s linear forwards var(--all-delay, 4.8s);
            &:before,
            &:after {
                content: '';
                position: absolute;
                z-index: 1;
                height: 2px;
                left: var(--l, 0);
                top: 15px;
                width: var(--w, 16px);
                background: var(--active);
                border-radius: 1px;
                transform-origin: var(--tx, 15px) 1px;
                transform: rotate(var(--before-rotate, 0deg));
                opacity: var(--tick-opacity, 0);
                transition: transform .4s ease var(--all-delay, 4.8s), opacity 0s linear var(--all-delay, 4.8s);
            }
            &:after {
                --l: 14px;
                --w: 26px;
                --tx: 1px;
                transform: rotate(var(--after-rotate, 0deg));
            }
            svg {
                stroke: var(--active);
                &.arrow {
                    width: 40px;
                    height: 32px;
                    opacity: var(--arrow-opacity, 1);
                    transition: opacity 0s linear var(--all-delay, 1s);
                }
                &.progress {
                    width: 444px;
                    height: 10px;
                    position: absolute;
                    left: 0;
                    top: 11px;
                    transform: translateX(var(--progress-x, 0));
                    opacity: var(--progress-opacity, 0);
                    transition: transform var(--all-transition, 4.4s) ease var(--all-delay, .4s), opacity 0s linear var(--all-delay, 1s);
                    animation: var(--hide, none) 0s linear forwards var(--all-delay, 4.8s);
                }
            }
        }
    }
    &.loading:not(.reset) {
        --line-y: -36px;
        --line-array: 0;
        --line-offset: 15px;
        --active-offset: 0;
        --arrow-opacity: 0;
        --progress-opacity: 1;
        --progress-x: -400px;
        --tick-opacity: 1;
        --before-rotate: 47deg;
        --after-rotate: -46deg;
        --hide: hide;
        --overflow: overflow;
        --icon-x: 2px;
        --icon-y: 7px;
        --count-opacity: 1;
        --count-y: 0;
        --count: count;
    }
    &.reset {
        --all-delay: 0s;
        --all-transition: .3s;
    }
}

@keyframes hide {
    to {
        opacity: 0;
    }
}

@keyframes count {
    to {
        transform: translateY(4px);
        opacity: 0;
    }
}

@keyframes overflow {
    to {
        overflow: visible;
    }
}

html {
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
}

* {
    box-sizing: inherit;
    &:before,
    &:after {
        box-sizing: inherit;
    }
}

// dribbble & twitter
body {
    min-height: 100vh;
    font-family: 'Inter', 'Inter UI', Arial;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #283044;
    font-family: 'Roboto', Arial, sans-serif;
    .dribbble {
        position: fixed;
        display: block;
        right: 20px;
        bottom: 20px;
        img {
            display: block;
            width: 76px;
        }
    }
    .twitter {
        position: fixed;
        display: block;
        right: 112px;
        bottom: 14px;
        svg {
            width: 24px;
            height: 24px;
            fill: white;
        }
    }
}

3. Javascript Code

const $ = (s, o = document) => o.querySelector(s);
const $$ = (s, o = document) => o.querySelectorAll(s);

$$('.button').forEach(button => {

    let count = {
            number: 0
        },
        icon = $('.icon', button),
        iconDiv = $('.icon > div', button),
        arrow = $('.icon .arrow', button),
        countElem = $('span', button),
        svgPath = new Proxy({
            y: null,
            s: null,
            f: null,
            l: null
        }, {
            set(target, key, value) {
                target[key] = value;
                if(target.y !== null && target.s != null && target.f != null && target.l != null) {
                    arrow.innerHTML = getPath(target.y, target.f, target.l, target.s, null);
                }
                return true;
            },
            get(target, key) {
                return target[key];
            }
        });

    svgPath.y = 30;
    svgPath.s = 0;
    svgPath.f = 8;
    svgPath.l = 32;

    button.addEventListener('click', e => {
        if(!button.classList.contains('loading')) {

            if(!button.classList.contains('animation')) {

                button.classList.add('loading', 'animation');

                gsap.to(svgPath, {
                    f: 2,
                    l: 38,
                    duration: .3,
                    delay: .15
                });

                gsap.to(svgPath, {
                    s: .2,
                    y: 16,
                    duration: .8,
                    delay: .15,
                    ease: Elastic.easeOut.config(1, .4)
                });

                gsap.to(count, {
                    number: '100',
                    duration: 3.8,
                    delay: .8,
                    onUpdate() {
                        countElem.innerHTML = Math.round(count.number) + '%';
                    }
                });

                setTimeout(() => {
                    iconDiv.style.setProperty('overflow', 'visible');
                    setTimeout(() => {
                        button.classList.remove('animation');
                    }, 600);
                }, 4820);

            }

        } else {

            if(!button.classList.contains('animation')) {

                button.classList.add('reset');

                gsap.to(svgPath, {
                    f: 8,
                    l: 32,
                    duration: .4
                });

                gsap.to(svgPath, {
                    s: 0,
                    y: 30,
                    duration: .4
                });

                setTimeout(() => {
                    button.classList.remove('loading', 'reset');
                    iconDiv.removeAttribute('style');
                }, 400);

            }

        }
        e.preventDefault();
    });

});

function getPoint(point, i, a, smoothing) {
    let cp = (current, previous, next, reverse) => {
            let p = previous || current,
                n = next || current,
                o = {
                    length: Math.sqrt(Math.pow(n[0] - p[0], 2) + Math.pow(n[1] - p[1], 2)),
                    angle: Math.atan2(n[1] - p[1], n[0] - p[0])
                },
                angle = o.angle + (reverse ? Math.PI : 0),
                length = o.length * smoothing;
            return [current[0] + Math.cos(angle) * length, current[1] + Math.sin(angle) * length];
        },
        cps = cp(a[i - 1], a[i - 2], point, false),
        cpe = cp(point, a[i - 1], a[i + 1], true);
    return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
}

function getPath(update, first, last, smoothing, pointsNew) {
    let points = pointsNew ? pointsNew : [
            [first, 16],
            [20, update],
            [last, 16]
        ],
        d = points.reduce((acc, point, i, a) => i === 0 ? `M ${point[0]},${point[1]}` : `${acc} ${getPoint(point, i, a, smoothing)}`, '');
    return `<path d="${d}" />`;
}

I hope you did find this tutorial useful!

For more web development or UI/UX design tutorials, follow us on:

Other useful resources: