← Illustrations

Floating Astronaut 404

A little astronaut tumbles slowly through space, waving, as they drift past a big 404 whose zero is a ringed planet. Stars twinkle behind. Made for “page not found” pages.

How to use it

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

Options

Add any of these attributes to the same element.

data-durationHow long the astronaut takes to drift across. Default 16000 ms.
data-colorColour of the planet and the visor. Default #a78bfa.

It is 8:5 with a deep-space background by default; set your own height or background on the element to change that. It pauses when off screen. With reduced motion the astronaut stays put.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Floating Astronaut 404: a little astronaut tumbles slowly through space,
    // waving, drifting past a big 404 whose zero is a ringed planet. Stars
    // twinkle behind. Drawn as SVG that fills the element, and paused when
    // off screen.
    // Usage: <div data-floating-astronaut-404></div>
    // It is 8:5 by default with a deep-space background; give it your own
    // height or background to change that.
    // Optional: data-duration (ms to drift across, default 16000),
    // data-color (planet and visor colour, default #a78bfa).
    var DEFAULTS = { duration: 16000, color: '#a78bfa' };
    var uid = 0;

    var css =
      '[data-floating-astronaut-404]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5;' +
      'background:radial-gradient(ellipse 80% 70% at 50% 45%,#2a1b5e 0%,#140f33 55%,#08071a 100%)}' +
      '.fa4-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}' +
      '.fa4-num{font:800 128px/1 system-ui,-apple-system,"Segoe UI",sans-serif;letter-spacing:-4px}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function build(el) {
      var d = el.dataset;
      var n = ++uid, id = 'fa4' + n;
      var color = d.color || DEFAULTS.color;
      var duration = +d.duration || DEFAULTS.duration;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', '404: an astronaut floating lost in space');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var stars = '';
      for (var i = 0; i < 90; i++) {
        stars += '<circle class="fa4-star" cx="' + (Math.random() * 1400 - 500).toFixed(1) + '" cy="' + (Math.random() * 500 - 125).toFixed(1) +
          '" r="' + (Math.random() * 1 + 0.4).toFixed(2) + '" fill="#fff" opacity="' + (Math.random() * 0.6 + 0.2).toFixed(2) + '"/>';
      }

      el.innerHTML =
        '<svg class="fa4-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs>' +
        '<linearGradient id="' + id + '-visor" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1e1b4b"/><stop offset="1" stop-color="' + color + '"/></linearGradient>' +
        '<radialGradient id="' + id + '-planet" cx=".35" cy=".3"><stop offset="0" stop-color="#fff" stop-opacity=".55"/><stop offset=".35" stop-color="' + color + '"/><stop offset="1" stop-color="#3b0764"/></radialGradient>' +
        '</defs>' +
        stars +
        '<circle cx="-40" cy="10" r="9" fill="#475569" opacity=".6"/><circle cx="440" cy="225" r="5" fill="#64748b" opacity=".5"/>' +
        // The 404: two 4s and a ringed planet for the 0.
        '<text class="fa4-num" x="112" y="170" text-anchor="middle" fill="#e9e3ff">4</text>' +
        '<text class="fa4-num" x="288" y="170" text-anchor="middle" fill="#e9e3ff">4</text>' +
        '<g transform="translate(200 124) rotate(-18)">' +
          '<path d="M-74 0A74 17 0 0 1 74 0" fill="none" stroke="#c4b5fd" stroke-width="5" opacity=".75"/>' +
          '<circle r="44" fill="url(#' + id + '-planet)"/>' +
          '<circle cx="-14" cy="12" r="7" fill="#000" opacity=".12"/><circle cx="16" cy="-14" r="4" fill="#000" opacity=".1"/>' +
          '<path d="M74 0A74 17 0 0 1 -74 0" fill="none" stroke="#c4b5fd" stroke-width="5" opacity=".95"/>' +
        '</g>' +
        // The astronaut, drawn around (0, 0).
        '<g class="fa4-astro"><g class="fa4-spin">' +
          '<rect x="-18" y="-10" width="36" height="32" rx="7" fill="#94a3b8"/>' +
          '<rect x="-28" y="-2" width="11" height="25" rx="5.5" fill="#e2e8f0" transform="rotate(28 -22 0)"/>' +
          '<rect x="-13" y="22" width="11" height="20" rx="5.5" fill="#e2e8f0" transform="rotate(12 -8 24)"/>' +
          '<rect x="2" y="22" width="11" height="20" rx="5.5" fill="#e2e8f0" transform="rotate(-20 8 24)"/>' +
          '<rect x="-15" y="-4" width="30" height="31" rx="10" fill="#f8fafc"/>' +
          '<rect x="-7" y="5" width="14" height="9" rx="2" fill="' + color + '"/>' +
          '<circle cx="-3" cy="9.5" r="1.4" fill="#fff"/><circle cx="2" cy="9.5" r="1.4" fill="#fde68a"/>' +
          '<g transform="translate(13 0)"><g class="fa4-wave"><rect x="-5.5" y="-24" width="11" height="25" rx="5.5" fill="#e2e8f0"/><circle cx="0" cy="-24" r="6" fill="#f8fafc"/></g></g>' +
          '<circle cx="0" cy="-20" r="18" fill="#f8fafc"/>' +
          '<rect x="-13" y="-30" width="26" height="18" rx="9" fill="url(#' + id + '-visor)"/>' +
          '<ellipse cx="-6" cy="-25" rx="4" ry="2.2" fill="#fff" opacity=".7"/>' +
        '</g></g>' +
        '</svg>';

      var svg = el.firstChild;
      var astro = svg.querySelector('.fa4-astro');
      var spin = svg.querySelector('.fa4-spin');
      var wave = svg.querySelector('.fa4-wave');

      function draw(t) {
        // Drift on a gentle diagonal from off the left to off the right.
        var k = (t % duration) / duration;
        var x = -90 + k * 580;
        var y = 200 - k * 150 + Math.sin(t / 1300) * 10;
        astro.setAttribute('transform', 'translate(' + x.toFixed(2) + ' ' + y.toFixed(2) + ') scale(1.25)');
        spin.setAttribute('transform', 'rotate(' + ((t / 70) % 360).toFixed(2) + ')');
        wave.setAttribute('transform', 'rotate(' + (Math.sin(t / 260) * 32 + 10).toFixed(2) + ')');
      }

      if (reduced()) {
        // A still frame: the astronaut floating just off the planet.
        astro.setAttribute('transform', 'translate(318 190) scale(1.25)');
        spin.setAttribute('transform', 'rotate(-24)');
        return;
      }

      var t = duration * 0.38, 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;
          draw(t);
        }
      });
      draw(t);
      var tw = anime.animate(svg.querySelectorAll('.fa4-star'), {
        opacity: function () { return anime.utils.random(0.05, 0.9, 2); },
        duration: function () { return anime.utils.random(1200, 3400); },
        delay: function () { return anime.utils.random(0, 2000); },
        ease: 'inOutSine',
        loop: true,
        alternate: true,
        autoplay: false
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); tw.play(); } else { timer.pause(); tw.pause(); }
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-floating-astronaut-404]').forEach(function (el) {
        if (el._fa4) return;   // idempotent
        el._fa4 = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-floating-astronaut-404></div>
<h1>Lost in space</h1>
<p>The page you were looking for has drifted off.</p>