About this snippet

The loading bar big sites show during navigation. A thin blue line races across the top, slows as it nears the end, then completes and fades out when the work is done.

The bar is a fixed element whose width is driven by a single percentage. Calling start sets it to a small value and begins a trickle, a timer that adds a shrinking random amount each tick so the bar always moves but never quite reaches the end on its own. That is what makes an unknown wait feel like progress. Calling done clears the timer, jumps the width to 100 percent, and after the transition finishes fades the bar out and resets it. A box-shadow on the same element gives the leading edge its glow.

Wire start to the beginning of a fetch and done to its completion, or to the navigation events of a single page app, and the bar reflects real requests instead of the demo timers.

Size
3.4 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
32
<div class="loader" id="loader" role="progressbar" aria-label="Page loading" aria-hidden="true"><div class="loader-bar" id="loader-bar"></div></div>
<main class="demo">
  <h1>Top loading bar</h1>
  <p>Trigger a request and watch the bar creep along the top edge, then complete once the response is in.</p>
  <div class="buttons">
    <button type="button" id="fast">Fast request</button>
    <button type="button" id="slow">Slow request</button>
  </div>
  <p class="status" id="status" aria-live="polite">Idle</p>
</main>
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: #f8fafc;
  color: #0f172a;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 20px;
}

.loader {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 100;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}

.loader.active {
  opacity: 1;
}

.loader-bar {
  width: 0;
  height: 100%;
  background: #2563eb;
  box-shadow: 0 0 10px #3b82f6, 0 0 4px #3b82f6;
  transition: width 0.3s ease;
}

.demo {
  max-width: 440px;
  text-align: center;
}

.demo h1 {
  font-size: 1.5rem;
  margin: 0 0 10px;
}

.demo p {
  color: #475569;
  line-height: 1.6;
  margin: 0 0 24px;
}

.buttons {
  display: flex;
  justify-content: center;
  gap: 10px;
}

.buttons button {
  font: inherit;
  font-weight: 600;
  padding: 11px 20px;
  border: 0;
  border-radius: 10px;
  background: #0f172a;
  color: #ffffff;
  cursor: pointer;
  transition: background 0.15s ease;
}

.buttons button:hover {
  background: #1e293b;
}

.buttons button:disabled {
  opacity: 0.5;
  cursor: default;
}

.status {
  margin: 20px 0 0 !important;
  font-size: 0.875rem;
  font-weight: 500;
}
const loader = document.getElementById("loader");
const bar = document.getElementById("loader-bar");
const status = document.getElementById("status");
const buttons = document.querySelectorAll(".buttons button");

let progress = 0;
let trickle = null;
let fade = null;
let reset = null;

function set(value) {
  progress = Math.min(value, 99.4);
  bar.style.width = progress + "%";
}

function start() {
  clearInterval(trickle);
  clearTimeout(fade);
  clearTimeout(reset);
  bar.style.transition = "";
  loader.classList.add("active");
  loader.setAttribute("aria-hidden", "false");
  set(8);
  trickle = setInterval(function() {
    const step = (100 - progress) * 0.08 * Math.random();
    set(progress + step);
  }, 250);
}

function done() {
  clearInterval(trickle);
  bar.style.width = "100%";
  fade = setTimeout(function() {
    loader.classList.remove("active");
    loader.setAttribute("aria-hidden", "true");
    reset = setTimeout(function() {
      bar.style.transition = "none";
      bar.style.width = "0";
      bar.offsetWidth;
      bar.style.transition = "";
      progress = 0;
    }, 400);
  }, 300);
}

function simulate(duration, label) {
  buttons.forEach(function(b) { b.disabled = true; });
  status.textContent = "Loading " + label;
  start();
  setTimeout(function() {
    done();
    status.textContent = "Loaded in " + (duration / 1000).toFixed(1) + " seconds";
    buttons.forEach(function(b) { b.disabled = false; });
  }, duration);
}

document.getElementById("fast").addEventListener("click", function() {
  simulate(1200, "the dashboard");
});

document.getElementById("slow").addEventListener("click", function() {
  simulate(4500, "the full report");
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save