← Illustrations

Butterfly

A butterfly flutters along a winding path over a little flower bed, flapping hard, gliding for a moment with its wings open, then flapping again. It leans into its turns and leaves a faint dotted trail.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-butterfly> 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 wings. Default #f472b6.
data-accentColour of the wing edges, spots and trail. Default #7c3aed.
data-speedSpeed multiplier. Default 1.
data-trail="false"No dotted trail.
data-flowers="false"No flower bed, just the butterfly.

It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion the butterfly rests in mid-air.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Butterfly: a butterfly flutters along a winding path over a little
    // flower bed, flapping hard, gliding for a moment, then flapping again,
    // and leaving a faint dotted trail. Drawn as SVG that fills the element,
    // and paused when off screen.
    // Usage: <div data-butterfly></div>
    // It is 8:5 by default on a transparent background.
    // Optional: data-color (wing colour, default #f472b6), data-accent (wing
    // edges and spots, default #7c3aed), data-speed (default 1),
    // data-trail="false" (no trail), data-flowers="false" (no flower bed).
    var DEFAULTS = { color: '#f472b6', accent: '#7c3aed', speed: 1 };
    var NS = 'http://www.w3.org/2000/svg';
    var TRAIL = 30;

    var css =
      '[data-butterfly]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.bfy-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; }

    // Where the butterfly is at time s (seconds): two slow sways each way.
    function pathAt(s) {
      return [
        200 + 138 * Math.sin(s * 0.42) + 18 * Math.sin(s * 1.3 + 0.5),
        100 + 42 * Math.sin(s * 0.77 + 1) + 14 * Math.sin(s * 2.1)
      ];
    }

    function wings(color, accent) {
      return '<path d="M0 -2C6 -22 30 -30 33 -14C35 -2 20 4 0 2Z" fill="' + color + '" stroke="' + accent + '" stroke-width="2.4" stroke-linejoin="round"/>' +
        '<path d="M0 3C16 4 27 12 21 23C15 32 3 20 0 7Z" fill="' + color + '" stroke="' + accent + '" stroke-width="2.4" stroke-linejoin="round"/>' +
        '<circle cx="22" cy="-14" r="4" fill="' + accent + '"/><circle cx="23" cy="-14" r="1.6" fill="#fff"/>' +
        '<circle cx="14" cy="15" r="2.6" fill="#fff" opacity=".85"/>' +
        '<path d="M2 -2C10 -10 18 -14 26 -16" fill="none" stroke="#fff" stroke-width="1.2" opacity=".5"/>';
    }

    // A leaf growing out of a stem at (x, y), pointing up and to side s.
    function leaf(x, y, s) {
      return 'M' + x + ' ' + y + 'C' + (x + s * 4) + ' ' + (y - 1) + ' ' + (x + s * 13) + ' ' + (y - 5) + ' ' + (x + s * 16) + ' ' + (y - 15) +
        'C' + (x + s * 8) + ' ' + (y - 15) + ' ' + (x + s * 2) + ' ' + (y - 9) + ' ' + x + ' ' + y + 'Z';
    }

    function flowerBed() {
      var html = '<g class="bfy-bed">';
      [[58, 50, '#f59e0b'], [118, 34, '#a78bfa'], [178, 56, '#fb7185'], [236, 30, '#fbbf24'], [292, 48, '#c084fc'], [348, 36, '#f472b6']].forEach(function (f, i) {
        var x = f[0], top = 232 - f[1];
        html += '<g class="bfy-flower" data-x="' + x + '"><path d="M' + x + ' 236Q' + (x + (i % 2 ? 6 : -6)) + ' ' + (top + 20) + ' ' + x + ' ' + top + '" fill="none" stroke="#16a34a" stroke-width="3" stroke-linecap="round"/>' +
          '<path d="' + leaf(x + (i % 2 ? 2 : -2), top + 30, i % 2 ? -1 : 1) + '" fill="#22c55e"/>' +
          '<g transform="translate(' + x + ' ' + top + ')" fill="' + f[2] + '">';
        for (var p = 0; p < 5; p++) {
          var a = p * Math.PI * 2 / 5;
          html += '<circle cx="' + (Math.cos(a) * 6).toFixed(2) + '" cy="' + (Math.sin(a) * 6).toFixed(2) + '" r="5.5"/>';
        }
        html += '<circle r="4" fill="#fde68a"/></g></g>';
      });
      html += '<path d="M10 238C80 232 320 232 390 238" stroke="#86efac" stroke-width="6" stroke-linecap="round" fill="none"/></g>';
      return html;
    }

    function build(el) {
      var d = el.dataset;
      var color = d.color || DEFAULTS.color;
      var accent = d.accent || DEFAULTS.accent;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A butterfly fluttering over flowers');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      el.innerHTML =
        '<svg class="bfy-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        (d.flowers === 'false' ? '' : flowerBed()) +
        '<g class="bfy-trail" fill="' + accent + '"></g>' +
        '<g class="bfy-fly"><g transform="scale(1.05)">' +
          '<g class="bfy-r">' + wings(color, accent) + '</g>' +
          '<g class="bfy-l">' + wings(color, accent) + '</g>' +
          '<ellipse cx="0" cy="4" rx="3" ry="14" fill="#1e1b2e"/><circle cx="0" cy="-11" r="3.6" fill="#1e1b2e"/>' +
          '<path d="M-1 -13C-3 -20 -6 -24 -9 -26M1 -13C3 -20 6 -24 9 -26" fill="none" stroke="#1e1b2e" stroke-width="1.3" stroke-linecap="round"/>' +
          '<circle cx="-9" cy="-26" r="1.6" fill="#1e1b2e"/><circle cx="9" cy="-26" r="1.6" fill="#1e1b2e"/>' +
        '</g></g>' +
        '</svg>';

      var svg = el.firstChild;
      var fly = svg.querySelector('.bfy-fly');
      var wr = svg.querySelector('.bfy-r'), wl = svg.querySelector('.bfy-l');
      var dots = [];
      if (d.trail !== 'false') {
        var tg = svg.querySelector('.bfy-trail');
        for (var i = 0; i < TRAIL; i++) {
          var c = document.createElementNS(NS, 'circle');
          c.setAttribute('r', 1.6);
          tg.appendChild(c);
          dots.push(c);
        }
      }
      var flowers = Array.prototype.slice.call(svg.querySelectorAll('.bfy-flower'));

      var phase = 0, rate = 0.05, gap = 0;
      var hist = [];
      function frame(dt, t) {
        var s = t / 1000;
        // Flap hard, then glide with wings held open now and then.
        var gliding = Math.sin(s * 0.9) > 0.72;
        rate += ((gliding ? 0.008 : 0.05) - rate) * Math.min(1, dt * 0.006);
        phase += rate * dt;
        var open = gliding ? 0.75 + 0.25 * Math.abs(Math.cos(phase)) : 0.18 + 0.82 * Math.abs(Math.cos(phase));
        wr.setAttribute('transform', 'scale(' + open.toFixed(3) + ' 1)');
        wl.setAttribute('transform', 'scale(' + (-open).toFixed(3) + ' 1)');

        var p = pathAt(s), q = pathAt(s + 0.05);
        var lift = Math.sin(phase * 2) * 2.2 * (gliding ? 0.2 : 1);
        var tilt = Math.max(-28, Math.min(28, (q[0] - p[0]) * 3.2));
        fly.setAttribute('transform', 'translate(' + p[0].toFixed(1) + ' ' + (p[1] + lift).toFixed(1) + ') rotate(' + tilt.toFixed(1) + ')');

        if (dots.length) {
          gap -= dt;
          if (gap <= 0) { gap = 70; hist.unshift([p[0], p[1] + 8]); if (hist.length > TRAIL) hist.pop(); }
          for (var i = 0; i < dots.length; i++) {
            var h = hist[i];
            if (!h || i < 2) { dots[i].setAttribute('opacity', 0); continue; }
            dots[i].setAttribute('cx', h[0].toFixed(1));
            dots[i].setAttribute('cy', h[1].toFixed(1));
            dots[i].setAttribute('opacity', (0.5 * (1 - i / dots.length)).toFixed(2));
          }
        }

        flowers.forEach(function (f, i) {
          var x = +f.getAttribute('data-x');
          f.setAttribute('transform', 'rotate(' + (Math.sin(s * 1.1 + i) * 2.5).toFixed(2) + ' ' + x + ' 236)');
        });
      }

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

      var t = 0, last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = (last ? Math.min(50, now - last) : 16) * speed;
          last = now;
          t += dt;
          frame(dt, 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-butterfly]').forEach(function (el) {
        if (el._bfy) return;   // idempotent
        el._bfy = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-butterfly></div>

<!-- a blue one, on its own: -->
<div data-butterfly data-color="#38bdf8" data-accent="#1e3a8a" data-flowers="false"></div>