Back to Blog

Matrix Digital Rain Effect in JavaScript (Canvas)

Celest KimCelest Kim

Video: Matrix Digital Rain Effect in JavaScript (Canvas) by CelesteAI

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

The falling green code from The Matrix is one of the most recognizable effects on the web — and one of the smallest to build. No library, no shaders. About forty lines of plain Canvas 2D.

This build uses TypeScript and Vite, scaffolded from the command line and run in the browser.

Set up the project

npm create vite@latest matrix-rain -- --template vanilla-ts
cd matrix-rain
npm install

No graphics library to install — Canvas is built into the browser. Trim index.html to a full-screen canvas on a near-black background:

<canvas id="bg"></canvas>
<script type="module" src="/src/main.ts"></script>

The glyphs and the columns

Grab the canvas and its 2D context. The iconic glyphs are half-width katakana — build them from character codes so you never have to paste non-ASCII into your source. Add the digits, and a helper to pick a random glyph.

const canvas = document.getElementById("bg") as HTMLCanvasElement;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;

const FONT_SIZE = 18;
const HEAD = "#c9ffe3";   // bright leading glyph
const TAIL = "#13b074";   // green trail

const charset: string[] = [];
for (let c = 0x30a0; c <= 0x30ff; c++) charset.push(String.fromCharCode(c)); // katakana
for (let c = 48; c <= 57; c++) charset.push(String.fromCharCode(c));         // digits
const glyph = () => charset[(Math.random() * charset.length) | 0];

The screen is split into columns, one falling stream each. The only state per column is a single number: which row its head is currently on. Start each at a random negative row so they stagger in.

let columns = 0;
let drops: number[] = [];

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  columns = Math.floor(canvas.width / FONT_SIZE);
  drops = Array.from({ length: columns }, () => (Math.random() * -50) | 0);
  ctx.fillStyle = "#020608";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}

The one trick: a translucent fill

Here’s the whole effect. Each frame, instead of clearing the canvas, paint a nearly transparent black rectangle over the entire thing. That dims whatever was already drawn by a small amount. Do it every frame and each glyph leaves a tail that fades out behind it as the canvas slowly darkens beneath it.

function frame() {
  ctx.fillStyle = "rgba(2, 6, 8, 0.08)";   // ← the fading tail
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.font = FONT_SIZE + "px monospace";

  for (let i = 0; i < columns; i++) {
    const x = i * FONT_SIZE;
    const y = drops[i] * FONT_SIZE;
    ctx.fillStyle = TAIL;
    ctx.fillText(glyph(), x, y);
    ctx.fillStyle = HEAD;
    ctx.fillText(glyph(), x, y + FONT_SIZE);   // brighter leading edge

    if (y > canvas.height && Math.random() > 0.975) drops[i] = 0;
    drops[i]++;
  }

  requestAnimationFrame(frame);
}

window.addEventListener("resize", resize);
resize();
frame();

The random drops[i] = 0 reset is what keeps the columns out of sync — without it they’d all fall in lockstep. A higher fade alpha (say 0.15) gives short tails; a lower one (0.04) gives long, ghostly streaks.

Run it

npm run dev

Open the local URL Vite prints and the code rains. Lower the z-index and put it behind your content for a cyberpunk background.

Recap

  • The fading tail is one translucent fill drawn over the canvas each frame — not a real clear. Tune its alpha to set the tail length.
  • Each column tracks one number — the row its head is on — and steps down every frame.
  • Random resets keep the columns out of sync, which is what makes it look organic instead of mechanical.

Want the deeper dive?

The full TypeScript project is on GitHub — clone it, npm install, npm run dev, and tune the glyphs, colors, and fade.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.