Number Loader in 3 Steps

GSAP-powered percentage loader that animates a vertical progress bar and a slot-machine-style number counter through three randomised intermediate values before reaching 100%. Clean, minimal aesthetic with a large typographic counter.

gsaploadercounterprogressnumberanimation

Setup — External Scripts

CDN — GSAP (add before </body>)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>

Code

HTML
html
<div class="loading-container">
  <div class="loading-screen">

    <!-- Vertical progress bar (left edge) -->
    <div class="loading__progress">
      <div class="loading__progress-inner"></div>
    </div>

    <!-- Slot-machine number counter -->
    <div class="loading__numbers">

      <!-- Hundreds digit (shows "1" only at 100%) -->
      <div class="loading__number-group is--first">
        <div class="loading__number-wrap">
          <span class="loading__number">1</span>
        </div>
      </div>

      <!-- Tens digit (0–9 strip) -->
      <div class="loading__number-group is--second">
        <div class="loading__number-wrap">
          <span class="loading__number">1</span>
          <span class="loading__number">2</span>
          <span class="loading__number">3</span>
          <span class="loading__number">4</span>
          <span class="loading__number">5</span>
          <span class="loading__number">6</span>
          <span class="loading__number">7</span>
          <span class="loading__number">8</span>
          <span class="loading__number">9</span>
          <span class="loading__number">0</span>
        </div>
      </div>

      <!-- Units digit (0–9 strip) -->
      <div class="loading__number-group is--third">
        <div class="loading__number-wrap">
          <span class="loading__number">1</span>
          <span class="loading__number">2</span>
          <span class="loading__number">3</span>
          <span class="loading__number">4</span>
          <span class="loading__number">5</span>
          <span class="loading__number">6</span>
          <span class="loading__number">7</span>
          <span class="loading__number">8</span>
          <span class="loading__number">9</span>
          <span class="loading__number">0</span>
        </div>
      </div>

      <!-- Percentage symbol -->
      <div class="loading__percentage-wrap">
        <span class="loading__percentage">%</span>
      </div>
    </div>

  </div>
</div>
CSS
css
.loading-container {
  z-index: 200;
  pointer-events: none;
  background-color: #E2E1DF;
  position: fixed;
  inset: 0;
  overflow: hidden;
}

.loading-screen {
  pointer-events: auto;
  background-color: #E2E1DF;
  width: 100%;
  height: 100%;
  display: none;
  position: absolute;
  top: 0;
  left: 0;
}

.loading__progress {
  width: 1em;
  height: 100%;
  position: absolute;
  bottom: 0;
  left: 0;
}

.loading__progress-inner {
  transform-origin: bottom;
  background-color: #ff4c24;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.loading__numbers {
  flex-flow: row;
  align-items: flex-start;
  font-size: calc(10vw + 10vh);
  display: flex;
  position: absolute;
  bottom: .1em;
  left: .23em;
}

.loading__number-group {
  flex-flow: column;
  height: 1em;
  display: flex;
  position: relative;
  overflow: hidden;
}

.loading__number-wrap {
  will-change: transform;
  flex-flow: column;
  display: flex;
  position: relative;
}

.loading__number {
  text-transform: uppercase;
  font-weight: 700;
  line-height: 1;
  position: relative;
}

.loading__percentage-wrap {
  flex-flow: column;
  justify-content: flex-start;
  margin-top: .375em;
  font-size: .3em;
  display: flex;
  overflow: hidden;
}

.loading__percentage {
  text-transform: uppercase;
  will-change: transform;
  font-weight: 700;
  line-height: 1;
  position: relative;
}
JavaScript
javascript
function initLoaderThreeSteps() {
  var tl = gsap.timeline();
  gsap.defaults({ ease: "Expo.easeInOut", duration: 1.2 });

  // Random intermediate values for tens and units digits
  var randomNumbers1 = gsap.utils.random([2, 3, 4]);
  var randomNumbers2 = gsap.utils.random([5, 6]);
  var randomNumbers3 = gsap.utils.random([1, 5]);
  var randomNumbers4 = gsap.utils.random([7, 8, 9]);

  // Show loader
  tl.set(".loading-screen", { display: "block" });
  tl.set(".loading__progress-inner", { scaleY: 0 });
  tl.set(
    ".loading__number-group.is--first .loading__number-wrap, .loading__percentage",
    { yPercent: 100 }
  );
  tl.set(
    ".loading__number-group.is--second .loading__number-wrap, .loading__number-group.is--third .loading__number-wrap",
    { yPercent: 10 }
  );

  // Step 1 — random value (e.g. 31%)
  tl.to(".loading__progress-inner", {
    scaleY: (randomNumbers1 + "" + randomNumbers3) / 100,
  });
  tl.to(".loading__percentage", { yPercent: 0 }, "<");
  tl.to(".loading__number-group.is--second .loading__number-wrap", {
    yPercent: (randomNumbers1 - 1) * -10,
  }, "<");
  tl.to(".loading__number-group.is--third .loading__number-wrap", {
    yPercent: (randomNumbers3 - 1) * -10,
  }, "<");

  // Step 2 — random value (e.g. 67%)
  tl.to(".loading__progress-inner", {
    scaleY: (randomNumbers2 + "" + randomNumbers4) / 100,
  });
  tl.to(".loading__number-group.is--second .loading__number-wrap", {
    yPercent: (randomNumbers2 - 1) * -10,
  }, "<");
  tl.to(".loading__number-group.is--third .loading__number-wrap", {
    yPercent: (randomNumbers4 - 1) * -10,
  }, "<");

  // Step 3 — 100%
  tl.to(".loading__progress-inner", { scaleY: 1 });
  tl.to(".loading__number-group.is--second .loading__number-wrap", {
    yPercent: -90,
  }, "<");
  tl.to(".loading__number-group.is--third .loading__number-wrap", {
    yPercent: -90,
  }, "<");
  tl.to(".loading__number-group.is--first .loading__number-wrap", {
    yPercent: 0,
  }, "<");
}

document.addEventListener("DOMContentLoaded", () => {
  initLoaderThreeSteps();
});

Attributes

NameTypeDefaultDescription
.loading__number-group.is--firstclassThe hundreds digit — contains a single "1". Starts off-screen (yPercent: 100) and slides in only when the counter reaches 100%.
.loading__number-group.is--secondclassThe tens digit strip (1–0). GSAP scrolls this vertically via yPercent to show the correct digit at each step.
.loading__number-group.is--thirdclassThe units digit strip (1–0). Works the same as the tens strip — yPercent determines which number is visible.
.loading__progress-innerclassThe vertical progress bar fill. transform-origin is set to bottom so scaleY: 0 → 1 fills upward.
.loading__percentageclassThe % symbol. Starts off-screen (yPercent: 100) and slides in alongside the first progress step.

Notes

  • Requires GSAP loaded via CDN before the script runs. No additional plugins needed.
  • The intermediate values are randomised on each page load using gsap.utils.random(), so the counter always shows a different two-step path to 100%.
  • The digit strips work by setting yPercent on the .loading__number-wrap. Each digit is 1em tall, so yPercent: -10 moves to the next digit. -90 = the "0" at the end of the strip.
  • The .loading-screen starts with display: none and is shown by the first GSAP .set() call — this prevents a flash of the loader before the script runs.
  • Colors (#E2E1DF background, #ff4c24 progress bar) are set directly in CSS — update them to match your design system.