15 Pure CSS Loading Animations
Progress Path
An honest progress bar built on the native `<progress>` element — semantic, screen-reader-announced, and bindable to real load progress (image preloads, fetch chunks, route transitions). A moving plane glides along the path, with the percentage announced via `aria-valuenow`. Degrades gracefully: a static bar appears if JS is disabled.
Progress Path the 3rd of 15 designs in the 15 Pure CSS Loading Animations collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<div class="pp-loader" role="status">
<div class="pp-track">
<progress class="pp-progress" value="0" max="100" aria-label="Page loading progress">
0%
</progress>
<span class="pp-glider" aria-hidden="true">
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 12l20-9-9 20-2-9-9-2z" />
</svg>
</span>
<span class="pp-pin pp-pin-start" aria-hidden="true"></span>
<span class="pp-pin pp-pin-end" aria-hidden="true"></span>
</div>
<div class="pp-meta">
<span class="pp-label">Preparing your tour</span>
<span class="pp-percent" aria-live="polite">0%</span>
</div>
</div> .pp-loader {
width: 240px;
display: grid;
gap: 10px;
font-family: system-ui, sans-serif;
}
.pp-track {
position: relative;
height: 28px;
display: grid;
align-items: center;
padding: 0 10px;
}
.pp-progress {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 4px;
border: 0;
border-radius: 99px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
color: #d4af37;
}
.pp-progress::-webkit-progress-bar {
background: rgba(255, 255, 255, 0.06);
border-radius: 99px;
}
.pp-progress::-webkit-progress-value {
background: linear-gradient(90deg, #5b8cb8, #d4af37);
border-radius: 99px;
transition: width 0.2s ease;
}
.pp-progress::-moz-progress-bar {
background: linear-gradient(90deg, #5b8cb8, #d4af37);
border-radius: 99px;
}
.pp-pin {
position: absolute;
top: 50%;
width: 6px;
height: 6px;
margin-top: -3px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
}
.pp-pin-start {
left: 7px;
background: #5b8cb8;
box-shadow: 0 0 8px rgba(91, 140, 184, 0.55);
}
.pp-pin-end {
right: 7px;
background: rgba(212, 175, 55, 0.45);
}
.pp-glider {
position: absolute;
top: 50%;
left: 10px;
width: 22px;
height: 22px;
margin-top: -11px;
display: grid;
place-items: center;
background: linear-gradient(135deg, #ffd479, #d4af37);
color: #1a1a2e;
border-radius: 50%;
box-shadow:
0 0 0 3px rgba(212, 175, 55, 0.18),
0 4px 14px -4px rgba(212, 175, 55, 0.7);
transform: translateX(0);
transition: transform 0.2s ease;
}
.pp-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.pp-label {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 600;
color: #b8b6d4;
}
.pp-percent {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
font-weight: 700;
color: #d4af37;
letter-spacing: 0.04em;
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.pp-glider {
transition: none;
}
.pp-progress::-webkit-progress-value {
transition: none;
}
} // Drives the indeterminate-then-determinate progress simulation.
// Replace this loop with your real fetch-progress callback.
document.querySelectorAll(".pp-loader").forEach(function (loader) {
var progress = loader.querySelector(".pp-progress");
var glider = loader.querySelector(".pp-glider");
var percent = loader.querySelector(".pp-percent");
if (!progress || !glider || !percent) return;
var v = 0;
function set(value) {
v = Math.max(0, Math.min(100, value));
progress.value = v;
percent.textContent = Math.round(v) + "%";
var track = loader.querySelector(".pp-track");
var max = track.offsetWidth - 20 - 22; // padding (20) + glider (22)
if (max < 0) max = 0;
glider.style.transform = "translateX(" + (max * v) / 100 + "px)";
}
function loop() {
set(0);
var step = 0;
var id = setInterval(function () {
step++;
var next = v + (step < 4 ? 18 : step < 8 ? 8 : 3);
set(next);
if (v >= 100) {
clearInterval(id);
setTimeout(loop, 1400);
}
}, 220);
}
loop();
});