← Illustrations

Unplugged Cable 404

A plug and its socket yank apart with a burst of sparks, swing on their cables and crackle, and the 404 above them loses power. Then they slide back together, click, and the lights come back on.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-unplugged-cable-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-textThe text above the cable. Default 404.
data-colorColour of the text. Default #6a0dad.
data-cableColour of the cables, plug and socket. Default #1e1b2e.
data-loop="false"Pull apart once and stay unplugged.

It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion it shows the cable already unplugged.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Unplugged Cable 404: a plug and its socket yank apart with a burst of
    // sparks, swing on their cables and crackle, and the 404 above them loses
    // power. Then they slide back together, click, and the lights come back
    // on. Drawn as SVG that fills the element, and paused when off screen.
    // Usage: <div data-unplugged-cable-404></div>
    // It is 8:5 by default on a transparent background.
    // Optional: data-color (the 404 and spark colour, default #6a0dad),
    // data-cable (cable and plug colour, default #1e1b2e), data-text
    // (default "404"), data-loop="false" (pull apart once and stay apart).
    var DEFAULTS = { color: '#6a0dad', cable: '#1e1b2e', text: '404' };
    var CYCLE = 6200;
    var uid = 0;

    var css =
      '[data-unplugged-cable-404]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.ucb-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}' +
      '.ucb-num{font:800 92px/1 system-ui,-apple-system,"Segoe UI",sans-serif;letter-spacing:-3px}';
    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 seg(t, a, b) { return clamp01((t - a) / (b - a)); }
    function inOut(k) { return k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2; }
    function outBack(k) { var c = 1.9; return 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); }

    function build(el) {
      var d = el.dataset;
      var id = 'ucb' + (++uid);
      var color = d.color || DEFAULTS.color;
      var cable = d.cable || DEFAULTS.cable;
      var loop = d.loop !== 'false';
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', (d.text || DEFAULTS.text) + ': a cable pulled out of its socket');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      function sparks(cls, rays) {
        var s = '<g class="' + cls + '" opacity="0">';
        for (var i = 0; i < rays; i++) {
          var a = (i / rays) * Math.PI * 2 + 0.3, r1 = 9, r2 = i % 2 ? 22 : 30;
          s += '<line x1="' + (Math.cos(a) * r1).toFixed(1) + '" y1="' + (Math.sin(a) * r1).toFixed(1) + '" x2="' + (Math.cos(a) * r2).toFixed(1) +
            '" y2="' + (Math.sin(a) * r2).toFixed(1) + '" stroke="' + (i % 2 ? '#facc15' : '#f97316') + '" stroke-width="3" stroke-linecap="round"/>';
        }
        return s + '<path d="M0 -8L2.5 -2.5L8 0L2.5 2.5L0 8L-2.5 2.5L-8 0L-2.5 -2.5Z" fill="#fde047"/></g>';
      }

      el.innerHTML =
        '<svg class="ucb-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs><linearGradient id="' + id + '-l" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="70" y2="0"><stop offset="0" stop-color="' + cable + '" stop-opacity="0"/><stop offset="1" stop-color="' + cable + '"/></linearGradient>' +
        '<linearGradient id="' + id + '-r" gradientUnits="userSpaceOnUse" x1="400" y1="0" x2="330" y2="0"><stop offset="0" stop-color="' + cable + '" stop-opacity="0"/><stop offset="1" stop-color="' + cable + '"/></linearGradient></defs>' +
        '<text class="ucb-num" x="200" y="96" text-anchor="middle" fill="' + color + '"></text>' +
        '<path class="ucb-cable-l" fill="none" stroke="url(#' + id + '-l)" stroke-width="7" stroke-linecap="round"/>' +
        '<path class="ucb-cable-r" fill="none" stroke="url(#' + id + '-r)" stroke-width="7" stroke-linecap="round"/>' +
        // The plug points right, the socket left; both drawn around their mouths.
        '<g class="ucb-plug">' +
          '<rect x="-2" y="-11" width="20" height="5" rx="1.5" fill="#cbd5e1"/><rect x="-2" y="6" width="20" height="5" rx="1.5" fill="#cbd5e1"/>' +
          '<path d="M-40 -20H-4a4 4 0 0 1 4 4V16a4 4 0 0 1 -4 4H-40a10 10 0 0 1 -10 -10V-10a10 10 0 0 1 10 -10Z" fill="' + cable + '"/>' +
          '<rect x="-36" y="-14" width="4" height="28" rx="2" fill="#fff" opacity=".15"/>' +
          '<rect x="-62" y="-6" width="14" height="12" rx="3" fill="' + cable + '"/>' +
        '</g>' +
        '<g class="ucb-sock">' +
          '<path d="M0 -24H38a10 10 0 0 1 10 10V14a10 10 0 0 1 -10 10H0Z" fill="' + cable + '"/>' +
          '<rect x="0" y="-24" width="6" height="48" rx="2" fill="#4b4563"/>' +
          '<rect x="2" y="-12" width="12" height="7" rx="2" fill="#0b0a12"/><rect x="2" y="5" width="12" height="7" rx="2" fill="#0b0a12"/>' +
          '<rect x="46" y="-6" width="14" height="12" rx="3" fill="' + cable + '"/>' +
        '</g>' +
        '<g class="ucb-at-gap">' + sparks('ucb-burst', 10) + '</g>' +
        '<g class="ucb-at-sock">' + sparks('ucb-fizz', 6) + '</g>' +
        '</svg>';

      var svg = el.firstChild;
      var num = svg.querySelector('.ucb-num');
      num.textContent = d.text || DEFAULTS.text;
      var plug = svg.querySelector('.ucb-plug'), sock = svg.querySelector('.ucb-sock');
      var cl = svg.querySelector('.ucb-cable-l'), cr = svg.querySelector('.ucb-cable-r');
      var atGap = svg.querySelector('.ucb-at-gap'), atSock = svg.querySelector('.ucb-at-sock');
      var burst = svg.querySelector('.ucb-burst'), fizz = svg.querySelector('.ucb-fizz');

      // gap: 0 plugged in, 1 fully apart. swing: tilt in degrees.
      function render(gap, swing, power, burstK, fizzK) {
        var droop = gap * 26;
        var px = 210 - gap * 92, py = 168 + droop + swing * 0.5;
        var sx = 212 + gap * 86, sy = 168 + droop - swing * 0.5;
        plug.setAttribute('transform', 'translate(' + px.toFixed(2) + ' ' + py.toFixed(2) + ') rotate(' + (swing + gap * 14).toFixed(2) + ')');
        sock.setAttribute('transform', 'translate(' + sx.toFixed(2) + ' ' + sy.toFixed(2) + ') rotate(' + (swing - gap * 14).toFixed(2) + ')');
        // Cables come in from the edges and sag down to the back of each end.
        var a = (swing + gap * 14) * Math.PI / 180, b = (swing - gap * 14) * Math.PI / 180;
        var lx = px - 60 * Math.cos(a), ly = py - 60 * Math.sin(a);
        var rx = sx + 58 * Math.cos(b), ry = sy + 58 * Math.sin(b);
        cl.setAttribute('d', 'M0 236C70 236 ' + (lx - 70).toFixed(1) + ' ' + (ly + 10).toFixed(1) + ' ' + lx.toFixed(1) + ' ' + ly.toFixed(1));
        cr.setAttribute('d', 'M400 236C330 236 ' + (rx + 70).toFixed(1) + ' ' + (ry + 10).toFixed(1) + ' ' + rx.toFixed(1) + ' ' + ry.toFixed(1));
        num.setAttribute('opacity', power.toFixed(3));
        atGap.setAttribute('transform', 'translate(' + ((px + sx) / 2 + 4).toFixed(1) + ' ' + ((py + sy) / 2).toFixed(1) + ')');
        atSock.setAttribute('transform', 'translate(' + (sx - 4).toFixed(1) + ' ' + sy.toFixed(1) + ')');
        burst.setAttribute('opacity', (burstK > 0 && burstK < 1 ? 1 - burstK : 0).toFixed(3));
        burst.setAttribute('transform', 'scale(' + (0.4 + burstK * 1.1).toFixed(3) + ')');
        fizz.setAttribute('opacity', (fizzK > 0 && fizzK < 1 ? 1 - fizzK : 0).toFixed(3));
        fizz.setAttribute('transform', 'scale(' + (0.3 + fizzK * 0.8).toFixed(3) + ') rotate(' + (fizzK * 40).toFixed(1) + ')');
      }

      // The story, in ms through a cycle.
      function frame(t) {
        var pull = seg(t, 900, 1500);
        var back = seg(t, 4300, 5200);
        var gap = outBack(pull) * (1 - inOut(back));
        // A damped swing after the yank.
        var since = t - 1200;
        var swing = since > 0 && t < 4300 ? 16 * Math.exp(-since / 900) * Math.sin(since / 170) : 0;
        // The 404 flickers out, dims, then flickers back on once plugged in.
        var power = 1;
        if (t > 900 && t < 5300) {
          var f = t - 900;
          power = f < 420 ? (Math.floor(f / 70) % 2 ? 0.25 : 0.9) : 0.22;
        } else if (t >= 5300 && t < 5600) {
          power = Math.floor((t - 5300) / 60) % 2 ? 0.35 : 1;
        }
        var burstK = seg(t, 1000, 1450);
        var fizzK = t < 2600 ? seg(t, 2100, 2500) : seg(t, 3300, 3650);
        if (back > 0.98) { burstK = seg(t, 5180, 5420) * 0.6; }
        render(gap, swing, power, burstK, fizzK);
      }

      if (reduced()) {
        // Unplugged and still.
        render(1, 0, 0.22, 0, 0);
        return;
      }

      var t = 0, last = 0, done = false;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          if (done) return;
          var now = performance.now();
          t += last ? Math.min(50, now - last) : 16;
          last = now;
          if (!loop && t >= 4200) { t = 4200; done = true; }
          frame(t % CYCLE);
        }
      });
      frame(0);
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }, { threshold: 0.2 }).observe(el);
    }

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

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-unplugged-cable-404></div>
<h1>Lost connection</h1>

<!-- once, with your own text: -->
<div data-unplugged-cable-404 data-text="500" data-loop="false"></div>