Back to CSS Loading Animations Progress Path CSS + JS
Share
HTML
<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>
CSS
.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;
  }
}
JS
// 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();
});