FreeBlockStats

Animated Stat Counter

Big numbers that count up when scrolled into view.

#stats#counter#kpi#animated#custom-liquid

Live preview

See it in action.

Fully interactive, drag, click, scroll inside the frame, toggle to mobile.

About this block

Stat counter with intersection-observer-triggered count-up animation. Add as many or as few stats as you need, just duplicate one .modblo-stat__cell block in the code.

Install in 90 seconds

  1. 01

    Open your Shopify admin and go to Online Store โ†’ Themes โ†’ Customize.

  2. 02

    On any template, click "Add section" where you want the block to appear.

  3. 03

    Choose "Custom Liquid" from the section list.

  4. 04

    Paste the code from this page into the Custom Liquid setting and click Save.

  5. 05

    Edit text and colors directly in the code (look for the comments marking edit points).

The code

Paste the snippet below into the Custom Liquid setting on a Custom Liquid section in your Theme Editor. Edit text, colors, and links directly in the code, every editable spot is marked with an EDIT comment.

modblo-stat-counter.liquid
{%- comment -%}
  modblo. Animated Stat Counter Block
  Paste into a Shopify "Custom Liquid" section via Theme Editor.
{%- endcomment -%}

<style>
  .modblo-stat { background:#0b0b0c; color:#fafafa; padding:clamp(48px,7vw,96px) 0; }   /* EDIT colors */
  .modblo-stat__inner { max-width:1100px; margin:0 auto; padding:0 24px; text-align:center; }
  .modblo-stat__h { font-size:clamp(24px,3vw,36px); letter-spacing:-.02em; margin:0 0 36px; }
  .modblo-stat__grid { display:grid; gap:24px; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); }
  .modblo-stat__cell { padding:16px; }
  .modblo-stat__num {
    font-size:clamp(36px,5vw,56px); font-weight:700; letter-spacing:-.025em; line-height:1;
    margin:0; color:#a78bfa;                                            /* EDIT number color */
    font-variant-numeric:tabular-nums;
  }
  .modblo-stat__lbl { font-size:14px; opacity:.7; margin:12px 0 0; letter-spacing:.04em; text-transform:uppercase; font-weight:500; }
</style>

<section class="modblo-stat">
  <div class="modblo-stat__inner">
    <h2 class="modblo-stat__h">By the numbers.</h2>                          {%- comment -%} EDIT heading {%- endcomment -%}
    <div class="modblo-stat__grid">
      {%- comment -%} EDIT each stat: data-target=number, prefix/suffix optional {%- endcomment -%}
      <div class="modblo-stat__cell">
        <p class="modblo-stat__num" data-target="1200" data-prefix="" data-suffix="+">0+</p>
        <p class="modblo-stat__lbl">Stores using us</p>
      </div>
      <div class="modblo-stat__cell">
        <p class="modblo-stat__num" data-target="75" data-prefix="" data-suffix="+">0+</p>
        <p class="modblo-stat__lbl">Premium sections</p>
      </div>
      <div class="modblo-stat__cell">
        <p class="modblo-stat__num" data-target="99" data-prefix="" data-suffix="/100">0/100</p>
        <p class="modblo-stat__lbl">Avg Lighthouse</p>
      </div>
    </div>
  </div>
</section>

<script>
  (function () {
    var nums = document.querySelectorAll('.modblo-stat__num');
    if (!('IntersectionObserver' in window)) {
      nums.forEach(function (n) {
        n.textContent = (n.dataset.prefix||'') + parseInt(n.dataset.target,10).toLocaleString() + (n.dataset.suffix||'');
      });
      return;
    }
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) {
        if (!e.isIntersecting) return;
        var el = e.target;
        var target = parseFloat(el.dataset.target) || 0;
        var prefix = el.dataset.prefix || '';
        var suffix = el.dataset.suffix || '';
        var dur = 1200, start = performance.now();
        function step(now) {
          var p = Math.min(1, (now - start) / dur);
          var eased = 1 - Math.pow(1 - p, 3);
          var val = Math.round(target * eased);
          el.textContent = prefix + val.toLocaleString() + suffix;
          if (p < 1) requestAnimationFrame(step);
        }
        requestAnimationFrame(step);
        io.unobserve(el);
      });
    }, { threshold: 0.4 });
    nums.forEach(function (n) { io.observe(n); });
  })();
</script>

What you can customize

Custom Liquid sections don't expose theme-editor settings, so customization happens in the code. Here's what to edit:

WhatHow to edit
HeadingFind the value in the code and change it directly.
Each statdata-target=number, data-prefix and data-suffix for $, %, +, /100, etc.
Number colorLook for /* EDIT number color */.

SEO & accessibility notes

  • Numbers animate via IntersectionObserver, only triggered when in view.
  • Falls back to static numbers in browsers without IO support.
  • Uses tabular-nums font feature for stable digit width during animation.