← Charts

Word Cloud Build

Words fly in from all sides, tumbling and growing, and settle into a tight cloud sized by their weight, the heaviest first. The cloud packs itself from the middle outwards and scales to fit its box, on a phone or a wide screen.

motioneasingtimingstaggerspringscrollhoverdelightSVGbouncefadeloopcanvasmorphdelayparallaxkeyframesrevealopacityphysicspathflowscalerotateblurtimelinetweenorbit

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-word-cloud-build to a container with a height, with one child per word and data-weight for its size.

Options

Add any of these attributes to the same element.

data-colorsComma-separated colours, used in turn. Default purple, pink, blue…
data-colorOn a word: its colour.
data-rotateTurn some of the words on their side.
data-durationTime for each word to fly in. Default 900 ms.
data-staggerGap between words. Default 60 ms.
data-loopScatter the words and build again.

Words that can’t find a place are left out, so very long lists are trimmed from the lightest end.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Word Cloud Build: words fly in from all sides, tumbling and growing, and
    // settle into a tight cloud sized by their weight, the heaviest first.
    // The cloud is packed from the middle outwards and scaled to fit its box.
    // Usage:
    //   <div data-word-cloud-build style="height:260px">
    //     <span data-weight="10">motion</span>
    //     <span data-weight="6">easing</span>
    //     <span data-weight="3">spring</span>
    //   </div>
    // Each child is one word (or short phrase); data-weight sets its size.
    // Optional: data-colors (comma-separated, used in turn, default purple,
    // pink, blue…), data-color on a word, data-rotate (turn some words on
    // their side), data-duration (ms for each word to fly in, default 900),
    // data-stagger (gap between words, default 60), data-loop (scatter and
    // build again).
    var DEFAULTS = { duration: 900, stagger: 60, hold: 2600 };
    var PALETTE = ['#a855f7', '#ec4899', '#0ea5e9', '#f59e0b', '#10b981', '#6366f1', '#6a0dad', '#f43f5e'];

    var css =
      '.wcl{position:relative;overflow:hidden}' +
      '.wcl-in{position:absolute;left:50%;top:50%;width:0;height:0}' +
      '.wcl-w{position:absolute;left:0;top:0;white-space:nowrap;font-weight:800;letter-spacing:-.02em;line-height:1;opacity:0;will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function setup(el) {
      var d = el.dataset;
      var colors = d.colors ? d.colors.split(',') : PALETTE;
      var words = [];
      Array.prototype.forEach.call(el.children, function (ch, i) {
        var t = ch.textContent.trim();
        if (!t) return;
        var w = parseFloat(ch.getAttribute('data-weight'));
        words.push({ text: t, weight: isNaN(w) ? 1 : w, color: ch.getAttribute('data-color') || colors[i % colors.length], order: i });
      });
      words.sort(function (a, b) { return b.weight - a.weight; });
      el.classList.add('wcl');
      el.setAttribute('role', 'img');
      el.setAttribute('aria-label', 'Word cloud: ' + words.map(function (w) { return w.text; }).join(', ') + '.');
      el.innerHTML = '';
      var inner = document.createElement('div');
      inner.className = 'wcl-in';
      inner.setAttribute('aria-hidden', 'true');
      el.appendChild(inner);
      words.forEach(function (w, i) {
        w.el = document.createElement('span');
        w.el.className = 'wcl-w';
        w.el.textContent = w.text;
        w.el.style.color = w.color;
        // Every third word after the biggest few turns on its side.
        w.side = d.rotate !== undefined && d.rotate !== 'false' && i > 2 && i % 3 === 0;
        inner.appendChild(w.el);
      });
      var c = { el: el, inner: inner, words: words };
      el._wcl = c;
      layout(c);
      return c;
    }

    function hits(a, list) {
      for (var i = 0; i < list.length; i++) {
        var b = list[i];
        if (a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y) return true;
      }
      return false;
    }

    // Lay the words out on a spiral from the middle, then scale the cloud to fit.
    function layout(c) {
      var W = c.el.clientWidth, H = c.el.clientHeight;
      if (!W || !H) return;
      c.W = W; c.H = H;
      var lo = Math.min.apply(null, c.words.map(function (w) { return w.weight; }));
      var hi = Math.max.apply(null, c.words.map(function (w) { return w.weight; }));
      var big = Math.max(18, Math.min(H * 0.3, W * 0.14, 72)), small = Math.max(10, big * 0.26);
      var placed = [], aspect = W / H, pad = Math.max(2, big * 0.06);
      c.words.forEach(function (w) {
        var f = hi > lo ? (w.weight - lo) / (hi - lo) : 1;
        w.size = small + Math.sqrt(f) * (big - small);
        w.el.style.fontSize = w.size.toFixed(1) + 'px';
        var bw = w.el.offsetWidth, bh = w.el.offsetHeight * 0.92;
        w.bw = bw; w.bh = bh;
        var rw = (w.side ? bh : bw) + pad, rh = (w.side ? bw : bh) + pad;
        w.placed = false;
        for (var i = 0; i < 10000; i++) {
          var a = i * 0.21, r = 3 * Math.sqrt(i);
          var cx = Math.cos(a) * r * Math.sqrt(aspect), cy = Math.sin(a) * r / Math.sqrt(aspect);
          var box = { x: cx - rw / 2, y: cy - rh / 2, w: rw, h: rh };
          if (!hits(box, placed)) { placed.push(box); w.cx = cx; w.cy = cy; w.placed = true; break; }
        }
      });
      var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
      placed.forEach(function (b) {
        minX = Math.min(minX, b.x); minY = Math.min(minY, b.y);
        maxX = Math.max(maxX, b.x + b.w); maxY = Math.max(maxY, b.y + b.h);
      });
      var s = Math.min(1.25, (W - 8) / (maxX - minX), (H - 8) / (maxY - minY));
      var ox = (minX + maxX) / 2, oy = (minY + maxY) / 2;
      c.inner.style.transform = 'scale(' + s.toFixed(3) + ')';
      c.scale = s;
      c.words.forEach(function (w) {
        w.el.style.display = w.placed ? '' : 'none';
        w.el.style.left = (w.cx - ox - w.bw / 2).toFixed(1) + 'px';
        w.el.style.top = (w.cy - oy - w.bh / 2).toFixed(1) + 'px';
        w.rot = w.side ? -90 : 0;
        if (c.done) anime.utils.set(w.el, { x: 0, y: 0, rotate: w.rot, scale: 1, opacity: 1 });
      });
    }

    function play(c) {
      var d = c.el.dataset;
      var dur = +d.duration || DEFAULTS.duration, stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var R = Math.max(c.W, c.H) * 0.75 / (c.scale || 1);
      c.done = false;
      var shown = c.words.filter(function (w) { return w.placed; });
      shown.forEach(function (w, i) {
        var a = Math.random() * Math.PI * 2;
        anime.animate(w.el, {
          x: [Math.cos(a) * R, 0],
          y: [Math.sin(a) * R, 0],
          rotate: [w.rot + anime.utils.random(-50, 50), w.rot],
          scale: [0.3, 1],
          opacity: { to: 1, duration: dur * 0.3, ease: 'linear' },
          duration: dur,
          delay: i * stagger,
          ease: 'out(4)'
        });
      });
      var total = dur + (shown.length - 1) * stagger;
      setTimeout(function () {
        c.done = true;
        if (d.loop === undefined || d.loop === 'false') return;
        setTimeout(function () {
          shown.forEach(function (w, i) {
            var a = Math.random() * Math.PI * 2;
            anime.animate(w.el, {
              x: Math.cos(a) * R, y: Math.sin(a) * R, rotate: w.rot + anime.utils.random(-60, 60), scale: 0.4, opacity: 0,
              duration: 700, delay: (shown.length - 1 - i) * 18, ease: 'in(3)'
            });
          });
          setTimeout(function () { play(c); }, 700 + shown.length * 18 + 300);
        }, DEFAULTS.hold);
      }, total);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target._wcl);
        });
      }, { threshold: 0.3 });
      var ro = new ResizeObserver(function (entries) {
        entries.forEach(function (e) {
          var c = e.target._wcl;
          if (c && (e.target.clientWidth !== c.W || e.target.clientHeight !== c.H)) layout(c);
        });
      });
      document.querySelectorAll('[data-word-cloud-build]').forEach(function (el) {
        if (el._wcl) return;
        var c = setup(el);
        ro.observe(el);
        if (reduced()) { c.done = true; layout(c); return; }
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-word-cloud-build style="height: 260px">
  <span data-weight="10">motion</span>
  <span data-weight="7">easing</span>
  <span data-weight="5">stagger</span>
  <span data-weight="3">spring</span>
  <span data-weight="2">delight</span>
</div>