← Backgrounds

Scrolling Code Wall

Columns of syntax-coloured code scroll slowly upwards behind a section, each at its own pace and fading out at the top and bottom, the quiet backdrop of a developer tool’s landing page. Use the built-in sample or your own code.

Built for developers

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-scrolling-code-wall to a section with a dark background.
  3. To show your own code, put it in a <template> inside the section.

Options

Add any of these attributes to the same element.

data-speedSpeed in px per second. Default 16.
data-sizeFont size in px. Default 13.
data-opacityHow strongly the code shows, from 0 to 1. Default 0.45.
data-directionup or down. Default up.

The code is painted once per column and then slid along, so it costs very little; there are more columns on wider screens. It’s hidden from screen readers and paused when off screen. With reduced motion it holds still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Scrolling Code Wall: columns of syntax-coloured code scroll slowly
    // upwards behind a section, each column at its own pace, fading out at the
    // top and bottom. Drawn on a transparent canvas, and paused when off screen.
    // Usage: <section data-scrolling-code-wall style="background:#0b1020">…</section>
    // Optional: data-speed (px per second, default 16), data-size (font size
    // in px, default 13), data-opacity (from 0 to 1, default 0.45),
    // data-direction ("up" or "down", default up).
    // Your own code: put it in a <template> inside the section and it is used
    // instead of the built-in sample.
    var DEFAULTS = { speed: 16, size: 13, opacity: 0.45 };

    var css =
      '[data-scrolling-code-wall]{position:relative;isolation:isolate;overflow:hidden}' +
      '.scw-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none;' +
      '-webkit-mask-image:linear-gradient(transparent,#000 22%,#000 78%,transparent);mask-image:linear-gradient(transparent,#000 22%,#000 78%,transparent)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var SAMPLE = [
      "import { animate, stagger } from 'animejs';",
      '',
      '// Bring each card in, one after another.',
      'export function revealCards(list) {',
      "  const cards = list.querySelectorAll('.card');",
      '  return animate(cards, {',
      '    opacity: [0, 1],',
      '    y: [24, 0],',
      '    delay: stagger(60),',
      "    ease: 'outQuart'",
      '  });',
      '}',
      '',
      'async function loadProjects(user) {',
      '  const res = await fetch(`/api/users/${user.id}/projects`);',
      "  if (!res.ok) throw new Error('Could not load projects');",
      '  const data = await res.json();',
      '  return data.items.filter((p) => !p.archived);',
      '}',
      '',
      'class Queue {',
      '  constructor(limit = 4) {',
      '    this.limit = limit;',
      '    this.running = 0;',
      '    this.jobs = [];',
      '  }',
      '  push(job) {',
      '    this.jobs.push(job);',
      '    this.next();',
      '  }',
      '  next() {',
      '    if (this.running >= this.limit) return;',
      '    const job = this.jobs.shift();',
      '    if (!job) return;',
      '    this.running += 1;',
      '    job().finally(() => {',
      '      this.running -= 1;',
      '      this.next();',
      '    });',
      '  }',
      '}',
      '',
      '// Debounce: wait until the user stops typing.',
      'function debounce(fn, wait = 200) {',
      '  let timer = null;',
      '  return (...args) => {',
      '    clearTimeout(timer);',
      '    timer = setTimeout(() => fn(...args), wait);',
      '  };',
      '}',
      '',
      "const theme = window.matchMedia('(prefers-color-scheme: dark)');",
      "document.body.dataset.theme = theme.matches ? 'dark' : 'light';",
      '',
      'for (const [key, value] of Object.entries(settings)) {',
      '  if (typeof value === \'number\') total += value * 0.5;',
      '}'
    ];

    var COLOURS = { comment: '#64748b', string: '#86efac', keyword: '#c084fc', number: '#fbbf24', call: '#67e8f9', name: '#e2e8f0', other: '#94a3b8' };
    var TOKEN = /(\/\/.*$)|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`[^`]*`)|\b(function|return|var|const|let|if|else|for|while|new|await|async|import|from|export|default|true|false|null|this|class|extends|of|in|typeof|throw)\b|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)|(\s+|.)/g;
    var KINDS = ['comment', 'string', 'keyword', 'number', 'call', 'name', 'other'];

    function tokens(line) {
      var out = [], m;
      TOKEN.lastIndex = 0;
      while ((m = TOKEN.exec(line))) {
        for (var k = 1; k <= 7; k++) if (m[k] !== undefined) { out.push([KINDS[k - 1], m[k]]); break; }
        if (!m[0]) TOKEN.lastIndex++;
      }
      return out;
    }

    function build(el) {
      var d = el.dataset;
      var speed = (d.speed !== undefined ? +d.speed : DEFAULTS.speed) * (d.direction === 'down' ? -1 : 1);
      var size = +d.size || DEFAULTS.size;
      var opacity = d.opacity !== undefined ? +d.opacity : DEFAULTS.opacity;
      var tpl = el.querySelector('template');
      var source = tpl ? tpl.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/^\n+|\s+$/g, '').split('\n') : SAMPLE;
      var lines = source.map(tokens);
      var canvas = document.createElement('canvas');
      canvas.className = 'scw-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var font = size + 'px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace';
      var lineH = Math.round(size * 1.65);
      var w = 0, h = 0, dpr = 1, columns = [], t = 0;
      var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

      // Paint one column of code, starting partway through the source so
      // neighbouring columns don't line up.
      function paintColumn(start, colW) {
        var c = document.createElement('canvas');
        var height = lines.length * lineH;
        c.width = Math.ceil(colW * dpr);
        c.height = Math.ceil(height * dpr);
        var g = c.getContext('2d');
        g.scale(dpr, dpr);
        g.font = font;
        g.textBaseline = 'middle';
        var cw = g.measureText('M').width;
        for (var i = 0; i < lines.length; i++) {
          var toks = lines[(start + i) % lines.length], x = 0, y = i * lineH + lineH / 2;
          for (var k = 0; k < toks.length; k++) {
            var text = toks[k][1];
            if (toks[k][0] !== 'other' || text.trim()) {
              g.fillStyle = COLOURS[toks[k][0]];
              g.fillText(text, x, y);
            }
            x += cw * text.length;
            if (x > colW) break;
          }
        }
        return { canvas: c, height: height };
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        var colW = Math.max(240, size * 32);
        var n = Math.max(1, Math.round(w / colW));
        var gutter = size * 2;
        colW = (w - gutter * (n + 1)) / n;
        columns = [];
        for (var i = 0; i < n; i++) {
          var col = paintColumn(Math.floor((i * lines.length) / n + i * 7), colW);
          col.x = gutter + i * (colW + gutter);
          col.w = colW;
          col.v = 0.8 + ((i * 37) % 10) / 25;
          col.off = (i * 131) % col.height;
          columns.push(col);
        }
        draw();
      }

      function draw() {
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.globalAlpha = opacity;
        for (var i = 0; i < columns.length; i++) {
          var c = columns[i];
          var y = -(((c.off + t * speed * c.v) % c.height + c.height) % c.height);
          for (; y < h; y += c.height) {
            ctx.drawImage(c.canvas, Math.round(c.x * dpr), Math.round(y * dpr));
          }
        }
        ctx.globalAlpha = 1;
      }

      resize();
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
      if (document.fonts && document.fonts.ready) document.fonts.ready.then(resize);
      if (still) return;

      var 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) / 1000;
          last = now;
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-scrolling-code-wall]').forEach(function (el) {
        if (el._scw) return;
        el._scw = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-scrolling-code-wall style="background: #0b1020; color: #fff">
  <h1>Built for developers</h1>
  <!-- optional: your own code -->
  <template>const hello = (name) => `Hello, ${name}!`;</template>
</section>