Loading

Spinner/loader with HTML, CSS and SVG

The idea is to duplicate content, fill it with flare color, position it absolutely to main content and hide unneccesary stuff with aria-hidden="true".

I use symbols to reduce HTML size by reusing SVGs insude use tags.

Tweak spinner animations inside <svg> and colors inside CSS!

Sadly, @property have no good support in Firefox (nightly build only) and Safari (supported since 27 Mar '23). So you can try against FF and Safari CSS properties with @supports at-rule and simply animate bacgkround-color for text span.

<svg class="visuallyhidden">
    <symbol id="icon-loader" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path style="stroke: var(--stroke);" opacity="1" d="M13.5352 6.46475L15.3031 4.69678" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="1; 1; 1; 0.43; 0.1; 0; 0.1; 0.43; 1"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="0.43" d="M17.5 10H15" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="0.43; 1; 1; 1; 0.43; 0.1; 0; 0.1; 0.43"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="0.1" d="M15.3031 15.3031L13.5352 13.5352" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="0.1; 0.43; 1; 1; 1; 0.43; 0.1; 0; 0.1"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="0" d="M10 17.5V15" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="0; 0.1; 0.43; 1; 1; 1; 0.43; 0.1; 0"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="0.1" d="M4.69727 15.3031L6.46523 13.5352" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="0.1; 0; 0.1; 0.43; 1; 1; 1; 0.43; 0.1"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="0.43" d="M2.5 10H5" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="0.43; 0.1; 0; 0.1; 0.43; 1; 1; 1; 0.43"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="1" d="M4.69727 4.69678L6.46523 6.46475" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="1; 0.43; 0.1; 0; 0.1; 0.43; 1; 1; 1"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
        <path style="stroke: var(--stroke);" opacity="1" d="M10 2.5V5" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round">
            <animate attributeName="opacity"
                        values="1; 1; 0.43; 0.1; 0; 0.1; 0.43; 1; 1"
                        keyTimes="0; 0.125; 0.25; 0.375; 0.5; 0.625; 0.75; 0.875; 1"
                        dur="2000ms"
                        calcMode="linear"
                        repeatCount="indefinite"></animate>
        </path>
    </symbol>
</svg>

<span class="loading">
    <span class="loading__text">Loading</span>
    <svg class="loading__icon">
        <use href="#icon-loader" />
    </svg>

    <span class="loading__fake" aria-hidden="true">
        <span class="loading__text">Loading</span>
        <svg class="loading__icon">
            <use href="#icon-loader" />
        </svg>
    </span>
</span>
.loading {
    --color: 219deg 100% 56%;
    --flare-color: 186deg 90% 54%; 

    display: inline-block;
    position: relative;

    width: max-content;
    height: max-content;

    --stroke: hsl(var(--color) / 1);
    color: var(--stroke);

    font-size: 4rem;
    font-weight: 500;

    &__icon {
        display: inline-block;
        width: 1em;
        aspect-ratio: 1;
        vertical-align: text-bottom;
    }

    &__fake {
        all: inherit;

        position: absolute;
        top: 0;
        left: 0;

        --stroke: hsl(var(--flare-color) / 1);
        color: var(--stroke);

        user-select: none;
        pointer-events: none;

        animation: inProgressFlare 6s ease-in 500ms infinite normal both;

        -webkit-mask-image: linear-gradient(
                54deg,
                rgb(0, 0, 0, 0) var(--loading-flare-left-trail, -30%),
                rgb(0, 0, 0, 1) var(--loading-flare-left, -25%),
                rgb(0, 0, 0, 1) var(--loading-flare-right, -5%),
                rgb(0, 0, 0, 0) var(--loading-flare-right-trail, 0%),
        );
        mask-image: linear-gradient(
                54deg,
                rgb(0, 0, 0, 0) var(--loading-flare-left-trail, -30%),
                rgb(0, 0, 0, 1) var(--loading-flare-left, -25%),
                rgb(0, 0, 0, 1) var(--loading-flare-right, -5%),
                rgb(0, 0, 0, 0) var(--loading-flare-right-trail, 0%),
        );
    }
}

@keyframes inProgressFlare {
    0% {
        --loading-flare-left-trail: -30%;
        --loading-flare-left: -25%;
        --loading-flare-right: -5%;
        --loading-flare-right-trail: 0%;
    }

    50%, 100% {
        --loading-flare-left-trail: 100%;
        --loading-flare-left: 105%;
        --loading-flare-right: 125%;
        --loading-flare-right-trail: 130%;
    }
}

@property --loading-flare-left-trail {
    syntax: "<percentage>";
    inherits: false;
    initial-value: -30%;
}

@property --loading-flare-left {
    syntax: "<percentage>";
    inherits: false;
    initial-value: -25%;
}

@property --loading-flare-right {
    syntax: "<percentage>";
    inherits: false;
    initial-value: -5%;
}

@property --loading-flare-right-trail {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 0%;
}

@property --loading-dash-opacity {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}
$ cd ..