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
- Paste the snippet just before
</head>. - Add
data-scrolling-code-wallto a section with a dark background. - To show your own code, put it in a
<template>inside the section.
Options
Add any of these attributes to the same element.
data-speed | Speed in px per second. Default 16. |
data-size | Font size in px. Default 13. |
data-opacity | How strongly the code shows, from 0 to 1. Default 0.45. |
data-direction | up 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.
<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(/</g, '<').replace(/>/g, '>').replace(/&/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><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>