← Diagrams & maps

Pyramid Build

Layers stack up into a pyramid. The base drops into place first, then each layer above lands on top with a little settle and its label fading in, until the peak is on.

Purpose Esteem Belonging Safety Food, water, rest

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-pyramid-build to a container with one child per layer, the top layer first.

Options

Add any of these attributes to the same element.

data-colorsComma-separated layer colours, top first. Default: shades of purple, darker towards the base.
data-staggerGap between layers landing. Default 280 ms.
data-loopLift off and stack up again.

It scales to the width of its container. Labels shrink to fit their layer, so keep the top one short.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Pyramid Build: layers stack up into a pyramid. The base drops into place
    // first, then each layer above it lands on top with a little settle, its
    // label fading in, until the peak is on. List the layers top first, as
    // you'd read them.
    // Usage:
    //   <div data-pyramid-build>
    //     <span>Purpose</span>
    //     <span>Growth</span>
    //     <span>Belonging</span>
    //     <span>Safety</span>
    //     <span>Basics</span>
    //   </div>
    // Optional: data-colors (comma-separated, top layer first; default shades
    // of purple), data-stagger (ms between layers, default 280), data-loop
    // (lift off and stack up again).
    var DEFAULTS = { top: [168, 85, 247], bottom: [76, 29, 149], stagger: 280, hold: 2400 };
    var NS = 'http://www.w3.org/2000/svg';

    var css =
      '.pyb{display:flex;align-items:center;justify-content:center}' +
      '.pyb-svg{display:block;width:100%;height:auto;max-height:100%;overflow:visible}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

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

    function shade(t) {
      var c = DEFAULTS.top.map(function (a, i) { return Math.round(a + (DEFAULTS.bottom[i] - a) * t); });
      return 'rgb(' + c.join(',') + ')';
    }

    function build(el) {
      var d = el.dataset;
      var names = Array.from(el.children).map(function (c) { return c.textContent.trim(); });
      var n = names.length;
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : null;
      var W = 320, lh = Math.min(48, 250 / Math.max(1, n)), gap = 4;
      var H = n * lh + (n - 1) * gap, T = 30, B = 156;
      function hw(y) { return T + ((B - T) * y) / H; }
      el.setAttribute('role', 'img');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Pyramid, from the top: ' + names.join(', '));
      el.textContent = '';
      el.classList.add('pyb');
      var svg = mk('svg', { class: 'pyb-svg', viewBox: '0 0 ' + W + ' ' + H, 'aria-hidden': 'true' }, el);
      var layers = names.map(function (name, i) {
        var y0 = i * (lh + gap), y1 = y0 + lh, cx = W / 2;
        var g = mk('g', {}, svg);
        var fill = colors ? colors[i % colors.length] : shade(n > 1 ? i / (n - 1) : 0);
        mk('polygon', { points: [cx - hw(y0), y0, cx + hw(y0), y0, cx + hw(y1), y1, cx - hw(y1), y1].join(' '), fill: fill, 'stroke-linejoin': 'round' }, g);
        var t = mk('text', { x: cx, y: (y0 + y1) / 2, 'text-anchor': 'middle', 'dominant-baseline': 'central', 'font-size': Math.min(15, lh * 0.38), 'font-weight': 700, fill: '#fff' }, g);
        t.textContent = name;
        var room = hw(y0) * 2 - 14, w = 0;
        try { w = t.getComputedTextLength(); } catch (e) {}
        if (w > room) t.setAttribute('font-size', Math.max(8, (Math.min(15, lh * 0.38) * room) / w));
        return { g: g, label: t };
      });
      return { layers: layers, loop: on(d.loop), done: false, visible: false };
    }

    function hide(s) {
      s.layers.forEach(function (l) {
        anime.utils.set(l.g, { y: -60, opacity: 0 });
        anime.utils.set(l.label, { opacity: 0 });
      });
    }

    function start(el) {
      var s = el._pyb;
      var stagger = +el.dataset.stagger || DEFAULTS.stagger;
      hide(s);
      var tl = anime.createTimeline({
        onComplete: function () {
          if (s.tl !== tl) return;
          if (s.loop) start(el); else s.done = true;
        }
      });
      s.tl = tl;
      var n = s.layers.length;
      // Bottom layer first.
      s.layers.slice().reverse().forEach(function (l, k) {
        var at = k * stagger;
        tl.add(l.g, { y: [-60, 0], duration: 620, ease: 'outBack(1.3)' }, at);
        tl.add(l.g, { opacity: [0, 1], duration: 260, ease: 'out(2)' }, at);
        tl.add(l.label, { opacity: [0, 1], duration: 350, ease: 'out(2)' }, at + 350);
      });
      var end = (n - 1) * stagger + 700;
      if (s.loop) {
        var back = end + DEFAULTS.hold;
        s.layers.forEach(function (l, i) {
          tl.add(l.g, { y: -40, opacity: 0, duration: 420, ease: 'in(2)' }, back + i * 90);
        });
      }
    }

    function init() {
      var still = reduced();
      document.querySelectorAll('[data-pyramid-build]').forEach(function (el) {
        if (el._pyb) return;
        var s = (el._pyb = build(el));
        if (still) return;
        hide(s);
        new IntersectionObserver(function (entries) {
          s.visible = entries[0].isIntersecting;
          if (s.visible) { if (!s.tl) start(el); else if (!s.done) s.tl.resume(); }
          else if (s.tl && !s.done) s.tl.pause();
        }, { threshold: 0.2 }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-pyramid-build style="max-width: 360px">
  <span>Purpose</span>
  <span>Growth</span>
  <span>Belonging</span>
  <span>Safety</span>
  <span>Basics</span>
</div>