← Loaders

Clock Spinner

A clock face with its hands spinning round at speed, like time on fast-forward. The minute hand whizzes round with a blur of ghost hands behind it and the hour hand creeps round a twelfth as fast. Size it with font-size and colour it with color.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-clock-spinner> where the loader should appear.

Options

Add any of these attributes to the same element.

data-speedTime for one turn of the minute hand. Default 1000 ms.
data-handColour of the minute hand. Default color.
data-ticks="false"Hide the hour marks.
data-trail="false"No blur behind the minute hand.
data-reverseRun the clock backwards.

It’s 2.4em across. It only moves while it’s on screen. With reduced motion the hands stand still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Clock Spinner: a clock face whose hands spin round at speed, the minute
    // hand whizzing round with a blur of ghost hands behind it and the hour
    // hand turning a twelfth as fast, like time running on fast-forward.
    // Sized with font-size (2.4em across), coloured with color.
    // It only moves while it's on screen.
    // Usage: <div data-clock-spinner aria-label="Loading"></div>
    // Optional: data-speed (ms for one turn of the minute hand, default 1000),
    // data-ticks="false" (no hour marks), data-trail="false" (no blur behind
    // the minute hand), data-hand (minute hand colour, default color),
    // data-reverse (run backwards).
    var DEFAULTS = { speed: 1000 };
    var SIZE = 2.4; // em

    var css =
      '.cks{display:inline-block;width:' + SIZE + 'em;height:' + SIZE + 'em;vertical-align:middle}' +
      '.cks svg{display:block;width:100%;height:100%;overflow:visible}' +
      '.cks-face{fill:currentColor;fill-opacity:.07;stroke:currentColor;stroke-width:3}' +
      '.cks-tick{stroke:currentColor;stroke-linecap:round;opacity:.5}' +
      '.cks-hand{stroke:currentColor;stroke-linecap:round}' +
      '.cks-hub{fill:currentColor}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var NS = 'http://www.w3.org/2000/svg';

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function make(parent, tag, attrs) {
      var n = document.createElementNS(NS, tag);
      for (var k in attrs) n.setAttribute(k, attrs[k]);
      parent.appendChild(n);
      return n;
    }

    function build(el) {
      var d = el.dataset;
      el.classList.add('cks');
      if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
      el.textContent = '';

      var svg = make(el, 'svg', { viewBox: '-25 -25 50 50', 'aria-hidden': 'true' });
      make(svg, 'circle', { class: 'cks-face', r: 22 });
      if (d.ticks !== 'false') {
        for (var i = 0; i < 12; i++) {
          var a = i * Math.PI / 6, big = i % 3 === 0;
          var r0 = big ? 14.5 : 16.5, r1 = 18.5;
          make(svg, 'line', {
            class: 'cks-tick',
            x1: (Math.sin(a) * r0).toFixed(2), y1: (-Math.cos(a) * r0).toFixed(2),
            x2: (Math.sin(a) * r1).toFixed(2), y2: (-Math.cos(a) * r1).toFixed(2),
            'stroke-width': big ? 2.4 : 1.6,
          });
        }
      }
      var hour = make(svg, 'line', { class: 'cks-hand', x1: 0, y1: 2.5, x2: 0, y2: -10, 'stroke-width': 3.4 });
      // Ghost copies trailing the minute hand read as motion blur.
      var ghosts = [];
      if (d.trail !== 'false') {
        [0.32, 0.18, 0.09].forEach(function (o) {
          var gh = make(svg, 'line', { class: 'cks-hand', x1: 0, y1: 0, x2: 0, y2: -15.5, 'stroke-width': 2.2 });
          gh.style.opacity = o;
          if (d.hand) gh.style.stroke = d.hand;
          ghosts.push(gh);
        });
      }
      var minute = make(svg, 'line', { class: 'cks-hand', x1: 0, y1: 3.5, x2: 0, y2: -16, 'stroke-width': 2.4 });
      if (d.hand) minute.style.stroke = d.hand;
      make(svg, 'circle', { class: 'cks-hub', r: 2.6 });

      var dir = d.reverse !== undefined && d.reverse !== 'false' ? -1 : 1;
      function draw(turns) {
        var m = dir * turns * 360;
        minute.setAttribute('transform', 'rotate(' + (m % 360).toFixed(2) + ')');
        hour.setAttribute('transform', 'rotate(' + (m / 12 % 360).toFixed(2) + ')');
        ghosts.forEach(function (g, i) {
          g.setAttribute('transform', 'rotate(' + ((m - dir * (i + 1) * 11) % 360).toFixed(2) + ')');
        });
      }

      draw(0.3 + 12 * 0.85);
      if (reduced()) {
        ghosts.forEach(function (g) { g.style.display = 'none'; });
        return null;
      }
      var speed = Math.max(200, +d.speed || DEFAULTS.speed);
      var t = (0.3 + 12 * 0.85) * speed, last = 0;
      return anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          t += last ? Math.min(50, now - last) : 16;
          last = now;
          draw(t / speed);
        },
        onPause: function () { last = 0; },
      });
    }

    function init() {
      document.querySelectorAll('[data-clock-spinner]').forEach(function (el) {
        if (el._cks) return;
        el._cks = true;
        var timer = build(el);
        if (!timer) return;
        new IntersectionObserver(function (entries) {
          entries[0].isIntersecting ? timer.play() : timer.pause();
        }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-clock-spinner style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>