← Text effects

Rotating Circular Text

A line of text set round a circle turns slowly, like a sticker or a “scroll down” seal. Whatever you put inside sits still in the middle, and the ring winds up to speed while the pointer is over it.

↓

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-rotating-circular-text="Your text •" to an element. Anything inside it, such as an icon or an arrow, sits in the middle.

Options

Add any of these attributes to the same element.

data-sizeThe badge’s width. Default 160 px.
data-speedTime for one full turn. Default 12000 ms.
data-directioncw (default) for clockwise, or ccw.
data-hover-speedHow much faster it turns while hovered. Default 4; 1 turns it off.

The text is spaced out to go exactly once round, so about 20 to 40 characters looks best. Repeat a short phrase to fill the ring.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Rotating Circular Text: a line of text set round a circle turns slowly,
    // like a badge or a "scroll down" seal. Whatever is inside the element sits
    // still in the middle, and the ring speeds up while the pointer is over it.
    // Usage: <div data-rotating-circular-text="Made with care • Made with care •">★</div>
    // Optional: data-size (width of the badge in px, default 160), data-speed
    // (ms for one full turn, default 12000), data-direction ("cw" (default) or
    // "ccw"), data-hover-speed (how much faster it turns on hover, default 4;
    // 1 turns that off).
    var DEFAULTS = { size: 160, speed: 12000, hoverSpeed: 4 };
    var NS = 'http://www.w3.org/2000/svg';
    var uid = 0;

    var css =
      '.rct{position:relative;display:inline-grid;place-items:center;aspect-ratio:1;vertical-align:middle}' +
      '.rct-ring{position:absolute;inset:0;width:100%;height:100%;overflow:visible}' +
      '.rct-ring text{fill:currentColor;font-size:10.5px;font-weight:700;text-transform:uppercase}' +
      '.rct-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function node(tag, attrs, parent) {
      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;
      var text = (el.getAttribute('data-rotating-circular-text') || 'Rotating text •').replace(/\s+/g, ' ').trim();
      el.classList.add('rct');
      if (!el.style.width) el.style.width = (+d.size || DEFAULTS.size) + 'px';
      var sr = document.createElement('span');
      sr.className = 'rct-sr';
      sr.textContent = text;
      el.insertBefore(sr, el.firstChild);

      // A circle drawn as a path from the top, clockwise. The text is stretched
      // to fit exactly once round, so the ring always closes up neatly.
      var r = 40;
      var id = 'rct-circle-' + ++uid;
      var svg = node('svg', { class: 'rct-ring', viewBox: '0 0 100 100', 'aria-hidden': 'true' }, el);
      node('path', { id: id, fill: 'none', d: 'M50 ' + (50 - r) + ' a' + r + ' ' + r + ' 0 1 1 0 ' + 2 * r + ' a' + r + ' ' + r + ' 0 1 1 0 ' + -2 * r }, svg);
      var label = node('text', { dy: '0.35em', textLength: (2 * Math.PI * r - 1).toFixed(1), lengthAdjust: 'spacing' }, svg);
      node('textPath', { href: '#' + id }, label).textContent = text + ' ';
      return svg;
    }

    function start(el, ring) {
      var d = el.dataset;
      var dir = d.direction === 'ccw' ? -1 : 1;
      var anim = anime.animate(ring, {
        rotate: [0, dir * 360],
        duration: +d.speed || DEFAULTS.speed,
        ease: 'linear',
        loop: true
      });
      // On hover the ring winds up to speed, and eases back down when you leave.
      var fast = +d.hoverSpeed || DEFAULTS.hoverSpeed;
      if (fast !== 1) {
        var to = function (v) { anime.animate(anim, { speed: v, duration: 600, ease: 'out(3)' }); };
        el.addEventListener('pointerenter', function () { to(fast); });
        el.addEventListener('pointerleave', function () { to(1); });
      }
      return anim;
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      document.querySelectorAll('[data-rotating-circular-text]').forEach(function (el) {
        if (el._rct) return;
        var ring = (el._rct = build(el));
        // Reduced motion: the badge stays still.
        if (reduced) return;
        var anim = null;
        // Turn only while visible, to save battery.
        new IntersectionObserver(function (entries) {
          if (!entries[0].isIntersecting) { if (anim) anim.pause(); return; }
          if (anim) anim.play();
          else anim = start(el, ring);
        }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<a href="#work" data-rotating-circular-text="Scroll to explore • Scroll to explore •">&darr;</a>