← Backgrounds

Matrix Rain

Columns of glowing glyphs pour down a section, each led by a bright, near-white head and trailing off into the dark, with characters flickering as they fall. The classic hacker-movie screen.

Enter the code

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-matrix-rain to a section with a dark background.

Options

Add any of these attributes to the same element.

data-colorGlyph colour. Default #4ade80.
data-sizeGlyph size in px; also the column width. Default 16.
data-speedSpeed multiplier. Default 1.
data-charsThe glyphs to use, e.g. 01 for binary. Default katakana and digits.

It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion it shows one still frame.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Matrix Rain: columns of glyphs fall down a section, each led by a bright
    // head and trailing off into the dark, with glyphs flickering as they go.
    // Drawn on a transparent canvas behind your content, and paused when off
    // screen.
    // Usage: <section data-matrix-rain style="background:#020a06">…</section>
    // Optional: data-color (default green #4ade80), data-size (glyph size in
    // px, default 16), data-speed (default 1), data-chars (the glyphs to use,
    // default katakana and digits).
    var DEFAULTS = {
      color: '#4ade80', size: 16, speed: 1,
      chars: 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'
    };

    var css =
      '[data-matrix-rain]{position:relative;isolation:isolate;overflow:hidden}' +
      '.mxr-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'mxr-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var color = d.color || DEFAULTS.color;
      var size = +d.size || DEFAULTS.size;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var chars = Array.from(d.chars || DEFAULTS.chars);
      var R = anime.utils.random;
      var w = 0, h = 0, dpr = 1, rows = 0;
      var cols = [];

      function glyph() { return chars[(Math.random() * chars.length) | 0]; }

      // A drop: a head that falls a row at a time, with a trail behind it.
      function drop(col, anywhere) {
        col.y = anywhere ? R(-rows, rows) : R(-rows * 0.8, -1);
        col.v = R(8, 22) * speed;     // rows per second
        col.len = R(8, 26, 0);
        col.dim = R(0.45, 1, 2);      // some columns are fainter, for depth
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        rows = Math.ceil(h / size) + 1;
        var n = Math.ceil(w / size);
        // Keep the columns we have, so a resize doesn't restart the rain.
        while (cols.length < n) {
          var c = { cells: [] };
          drop(c, true);
          cols.push(c);
        }
        cols.length = n;
        cols.forEach(function (c) { while (c.cells.length < rows) c.cells.push(glyph()); c.cells.length = rows; });
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.font = '600 ' + size + 'px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        for (var i = 0; i < cols.length; i++) {
          var c = cols[i], head = Math.floor(c.y), x = i * size + size / 2;
          for (var k = c.len; k >= 0; k--) {
            var r = head - k;
            if (r < 0 || r >= rows) continue;
            if (k === 0) {
              // The head glows near-white.
              ctx.globalAlpha = c.dim;
              ctx.fillStyle = '#ecfdf5';
              ctx.shadowColor = color;
              ctx.shadowBlur = size * 0.8;
            } else {
              var f = 1 - k / (c.len + 1);
              ctx.globalAlpha = c.dim * f * f * 0.95;
              ctx.fillStyle = color;
              ctx.shadowBlur = 0;
            }
            ctx.fillText(c.cells[r], x, r * size);
          }
        }
        ctx.shadowBlur = 0;
        ctx.globalAlpha = 1;
      }

      function advance(dt) {
        for (var i = 0; i < cols.length; i++) {
          var c = cols[i];
          var before = Math.floor(c.y);
          c.y += (c.v * dt) / 1000;
          // A new glyph appears under the head each time it moves down a row.
          if (Math.floor(c.y) !== before && Math.floor(c.y) >= 0 && Math.floor(c.y) < rows) c.cells[Math.floor(c.y)] = glyph();
          if (c.y - c.len > rows) drop(c, false);
          // And a few in the trail flicker to something else.
          if (Math.random() < dt * 0.004) c.cells[(Math.random() * rows) | 0] = glyph();
        }
      }

      resize();
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        draw();
        if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      // Glyphs move a whole row at a time, so redraw about 30 times a second.
      var last = 0, wait = 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;
          last = now;
          advance(dt);
          wait += dt;
          if (wait < 33) return;
          wait = 0;
          draw();
        }
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-matrix-rain]').forEach(function (el) {
        if (el._mxr) return;
        el._mxr = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-matrix-rain style="background: #020a06; color: #fff">
  <h1>Enter the code</h1>
</section>