← Illustrations

Growing Plant

A seed wobbles in a flowerpot, then a shoot pushes up and its leaves unfurl one by one as the stem climbs. A flower opens at the top and the whole plant sways in a gentle breeze, before it fades and grows again.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-growing-plant> where the scene should go. It fills the element with an SVG scene.

Options

Add any of these attributes to the same element.

data-colorColour of the leaves. Default #22c55e.
data-flowerColour of the flower, or false for just leaves. Default #f472b6.
data-potColour of the pot. Default #c2410c.
data-durationTime for one grow, hold and fade. Default 9000 ms.
data-loop="false"Grow once and stay in flower.

It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion it shows the plant fully grown.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Growing Plant: a seed in a flowerpot sprouts, the stem climbs and its
    // leaves unfurl in pairs on the way up, then a flower opens at the top.
    // It sways gently in a breeze. Drawn as SVG that fills the element, and
    // paused when off screen.
    // Usage: <div data-growing-plant></div>
    // It is 8:5 by default on a transparent background.
    // Optional: data-color (leaf colour, default #22c55e), data-flower
    // (flower colour, default #f472b6; "false" for no flower), data-pot (pot
    // colour, default #c2410c), data-duration (one grow, hold and fade, ms,
    // default 9000), data-loop="false" (grow once and stay).
    var DEFAULTS = { color: '#22c55e', flower: '#f472b6', pot: '#c2410c', duration: 9000 };
    var NS = 'http://www.w3.org/2000/svg';
    var STEM = 'M200 184C196 160 208 132 200 108C194 88 204 72 200 56';
    // Where each leaf sits along the stem (0 = soil, 1 = tip) and its size.
    var LEAVES = [[0.2, 1], [0.32, 0.95], [0.5, 0.85], [0.62, 0.8], [0.78, 0.66], [0.86, 0.6]];

    var css =
      '[data-growing-plant]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.gpl-svg{position:absolute;inset:0;width:100%;height:100%;display:block;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 clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
    function outCubic(k) { k = clamp01(k); return 1 - Math.pow(1 - k, 3); }
    function outBack(k) { k = clamp01(k); var c = 1.6; return 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); }
    function inOut(k) { k = clamp01(k); return k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2; }

    function build(el) {
      var d = el.dataset;
      var leaf = d.color || DEFAULTS.color;
      var flowerCol = d.flower === 'false' ? null : (d.flower || DEFAULTS.flower);
      var pot = d.pot || DEFAULTS.pot;
      var dur = Math.max(3000, +d.duration || DEFAULTS.duration);
      var loop = d.loop !== 'false';
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A seedling sprouting in a flowerpot');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var petals = '';
      for (var i = 0; i < 6; i++) petals += '<ellipse cx="0" cy="-10" rx="6" ry="10" transform="rotate(' + i * 60 + ')"/>';

      el.innerHTML =
        '<svg class="gpl-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<ellipse cx="200" cy="240" rx="70" ry="6" fill="#1e1b2e" opacity=".08"/>' +
        '<path d="M157 196L243 196L235 238L165 238Z" fill="' + pot + '"/>' +
        '<path d="M157 196L243 196L241 206L159 206Z" fill="#000" opacity=".12"/>' +
        '<rect x="150" y="178" width="100" height="20" rx="4" fill="' + pot + '"/>' +
        '<rect x="150" y="178" width="100" height="5" rx="2.5" fill="#fff" opacity=".18"/>' +
        '<ellipse cx="200" cy="182" rx="45" ry="5.5" fill="#57301a"/>' +
        '<ellipse class="gpl-seed" cx="200" cy="180" rx="6" ry="4" fill="#a16207"/>' +
        '<g class="gpl-plant">' +
          '<path class="gpl-stem" d="' + STEM + '" fill="none" stroke="#15803d" stroke-width="4" stroke-linecap="round"/>' +
          '<g class="gpl-leaves"></g>' +
          '<g class="gpl-top" transform="translate(200 56)">' +
            '<path class="gpl-bud" d="M0 4C-7 0 -6 -10 0 -16C6 -10 7 0 0 4Z" fill="' + leaf + '"/>' +
            (flowerCol ? '<g class="gpl-flower"><g fill="' + flowerCol + '">' + petals + '</g><circle r="6" fill="#fbbf24"/><circle r="2.5" cx="-1.5" cy="-1.5" fill="#fde68a"/></g>' : '') +
          '</g>' +
        '</g>' +
        '</svg>';

      var svg = el.firstChild;
      var plant = svg.querySelector('.gpl-plant');
      var stem = svg.querySelector('.gpl-stem');
      var seed = svg.querySelector('.gpl-seed');
      var bud = svg.querySelector('.gpl-bud');
      var flower = svg.querySelector('.gpl-flower');
      var leavesG = svg.querySelector('.gpl-leaves');
      var L = stem.getTotalLength();
      stem.setAttribute('stroke-dasharray', L.toFixed(1) + ' ' + (L + 2).toFixed(1));

      var leaves = LEAVES.map(function (spec, i) {
        var p = stem.getPointAtLength(spec[0] * L);
        var g = document.createElementNS(NS, 'g');
        g.innerHTML = '<path d="M0 0C8 -10 28 -12 38 0C28 12 8 10 0 0Z" fill="' + leaf + '"/>' +
          '<path d="M3 0L32 0" stroke="#15803d" stroke-width="1.2" stroke-linecap="round" opacity=".55"/>';
        leavesG.appendChild(g);
        return { el: g, f: spec[0], size: spec[1], x: p.x, y: p.y, side: i % 2 ? -1 : 1 };
      });

      function frame(p, t) {
        // Seed: settles in, wobbles, then the sprout takes over.
        seed.setAttribute('opacity', (clamp01(p / 0.03) * (1 - clamp01((p - 0.1) / 0.04))).toFixed(2));
        seed.setAttribute('transform', 'rotate(' + (Math.sin(t * 0.03) * 12 * clamp01((p - 0.04) / 0.02)).toFixed(1) + ' 200 182)');

        var g = outCubic((p - 0.08) / 0.5);
        stem.setAttribute('stroke-dashoffset', (L * (1 - g)).toFixed(1));
        stem.setAttribute('opacity', g > 0 ? 1 : 0);
        leaves.forEach(function (lf) {
          var u = outCubic((g - lf.f) / 0.24);
          if (u <= 0) { lf.el.setAttribute('opacity', 0); return; }
          // Unfurl: starts pointing up and narrow, opens out sideways.
          var ang = lf.side > 0 ? -26 - (1 - u) * 56 : -154 + (1 - u) * 56;
          var s = lf.size * (0.08 + 0.92 * u);
          lf.el.setAttribute('opacity', 1);
          lf.el.setAttribute('transform', 'translate(' + lf.x.toFixed(1) + ' ' + lf.y.toFixed(1) + ') rotate(' + ang.toFixed(1) + ') scale(' + s.toFixed(3) + ' ' + (s * Math.pow(u, 0.6)).toFixed(3) + ')');
        });
        var b = outBack((g - 0.92) / 0.08);
        var q = flower ? outBack((p - 0.6) / 0.14) : 0;
        bud.setAttribute('transform', 'scale(' + Math.max(0.001, b * (1 - q * 0.6)).toFixed(3) + ')');
        if (flower) flower.setAttribute('transform', 'rotate(' + ((1 - q) * -60).toFixed(1) + ') scale(' + Math.max(0.001, q).toFixed(3) + ')');

        var sway = Math.sin(t * 0.0013) * 2.4 + Math.sin(t * 0.0031) * 0.8;
        plant.setAttribute('transform', 'rotate(' + (sway * g).toFixed(2) + ' 200 184)');
        plant.setAttribute('opacity', (1 - inOut((p - 0.9) / 0.08)).toFixed(3));
      }

      if (reduced()) { frame(0.8, 0); return; }

      var t = 0, last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          t += last ? Math.min(50, now - last) : 16;
          last = now;
          var p = loop ? (t % dur) / dur : Math.min(t / dur, 0.82);
          frame(p, t);
        }
      });
      frame(0, 0);
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-growing-plant]').forEach(function (el) {
        if (el._gpl) return;   // idempotent
        el._gpl = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-growing-plant></div>
<h2>Your garden is empty</h2>

<!-- grow once, no flower: -->
<div data-growing-plant data-loop="false" data-flower="false"></div>