Matrix Code Rain Canvas Background

The falling code effect, drawn on a single canvas that fills its section. Each column drops random letters, digits and symbols. The newest character in a column is nearly white and the ones above it fade to green and then to black, which gives the trails their depth. A dark radial pool sits between the rain and the content so text stays readable. The demo is a security firm hero with a headline, two buttons and a small findings panel. On phones the panel moves under the copy and the rain simply uses fewer columns.

Code

// Adapted from Eldora UI (https://github.com/karthikmudunuri/eldoraui/blob/main/apps/www/registry/eldoraui/hacker-background.tsx)
// Copyright (c) Mudunuri Bhaskara Karthikeya Varma. MIT License: https://we.inc/components/licenses
import { useEffect, useRef } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { ArrowRight, Terminal } from "lucide-react";

const rain = {
  color: "#22ff88", // trail colour
  head: "#eafff3", // the leading character of each column
  fontSize: 15,
  fps: 24,
  chars: "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789<>/\\[]{}=+*#$%&",
};

const findings = [
  { id: "BLK-0412", text: "Password reset link never expires", level: "High" },
  { id: "BLK-0409", text: "Admin panel reachable without two-step sign-in", level: "High" },
  { id: "BLK-0398", text: "Verbose error page leaks stack trace", level: "Low" },
];

/** Falling columns of characters drawn on a canvas that fills its parent. */
function CodeRain() {
  const ref = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = ref.current;
    const ctx = canvas?.getContext("2d");
    if (!canvas || !ctx) return;
    const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const pick = () => rain.chars[Math.floor(Math.random() * rain.chars.length)];

    let w = 0;
    let h = 0;
    let drops: number[] = [];
    let last: string[] = [];

    const step = () => {
      // A translucent black wash fades older characters, which draws the trails.
      ctx.fillStyle = "rgba(2, 8, 5, 0.085)";
      ctx.fillRect(0, 0, w, h);
      ctx.font = `${rain.fontSize}px ui-monospace, SFMono-Regular, Menlo, monospace`;
      for (let i = 0; i < drops.length; i++) {
        const x = i * rain.fontSize;
        const y = drops[i] * rain.fontSize;
        ctx.fillStyle = rain.color;
        ctx.fillText(last[i], x, y - rain.fontSize);
        last[i] = pick();
        ctx.fillStyle = rain.head;
        ctx.fillText(last[i], x, y);
        if (y > h && Math.random() > 0.972) drops[i] = 0;
        drops[i] += 1;
      }
    };

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      w = canvas.clientWidth;
      h = canvas.clientHeight;
      canvas.width = Math.round(w * dpr);
      canvas.height = Math.round(h * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.textBaseline = "top";
      const columns = Math.ceil(w / rain.fontSize);
      const rows = Math.ceil(h / rain.fontSize);
      drops = Array.from({ length: columns }, () => Math.floor(Math.random() * rows * 1.6) - rows * 0.6);
      last = Array.from({ length: columns }, pick);
      ctx.fillStyle = "#020805";
      ctx.fillRect(0, 0, w, h);
      // Run ahead so the first visible frame already has full trails.
      for (let i = 0; i < 70; i++) step();
    };
    resize();

    let raf = 0;
    let before = 0;
    let visible = true;
    const loop = (now: number) => {
      raf = requestAnimationFrame(loop);
      if (now - before < 1000 / rain.fps) return;
      before = now;
      step();
    };
    const start = () => {
      cancelAnimationFrame(raf);
      if (!still && visible && !document.hidden) raf = requestAnimationFrame(loop);
    };

    const sizeWatcher = new ResizeObserver(resize);
    sizeWatcher.observe(canvas);
    const viewWatcher = new IntersectionObserver(([entry]) => {
      visible = entry.isIntersecting;
      start();
    });
    viewWatcher.observe(canvas);
    document.addEventListener("visibilitychange", start);
    start();

    return () => {
      cancelAnimationFrame(raf);
      sizeWatcher.disconnect();
      viewWatcher.disconnect();
      document.removeEventListener("visibilitychange", start);
    };
  }, []);

  return <canvas ref={ref} aria-hidden="true" className="pointer-events-none absolute inset-0 h-full w-full" />;
}

export default function MatrixCodeRainCanvasBackground() {
  const reduce = useReducedMotion();
  const rise = (delay: number) => ({
    initial: reduce ? false : { opacity: 0, y: 20 },
    animate: { opacity: 1, y: 0 },
    transition: { type: "spring" as const, stiffness: 110, damping: 20, delay },
  });

  return (
    <section className="relative isolate overflow-hidden bg-[#020805] px-4 py-20 text-white sm:px-6 sm:py-28">
      <CodeRain />
      {/* A dark pool behind the copy keeps it readable over the moving characters. */}
      <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(75%_70%_at_42%_48%,rgba(2,8,5,0.94)_35%,rgba(2,8,5,0.55)_70%,transparent)]" aria-hidden="true" />

      <div className="relative mx-auto grid max-w-6xl items-center gap-12 lg:grid-cols-[1.1fr_0.9fr]">
        <div>
          <motion.p {...rise(0)} className="inline-flex items-center gap-2 rounded-md border border-emerald-400/30 bg-emerald-400/10 px-3 py-1.5 font-mono text-xs uppercase tracking-widest text-emerald-300">
            <Terminal className="h-3.5 w-3.5" aria-hidden="true" /> Blackmere offensive security
          </motion.p>
          <motion.h1 {...rise(0.08)} className="mt-6 text-4xl font-semibold tracking-tight sm:text-6xl">
            We break in on Monday so nobody else does on Friday
          </motion.h1>
          <motion.p {...rise(0.16)} className="mt-6 max-w-xl text-lg leading-relaxed text-emerald-50/80">
            A two week penetration test of your web app, run by people, not scanners. You get a ranked list of what we found, how we got in and the exact fix for each.
          </motion.p>
          <motion.div {...rise(0.24)} className="mt-9 flex flex-wrap gap-3">
            <a href="#" className="inline-flex h-12 items-center gap-2 rounded-md bg-emerald-400 px-6 text-sm font-semibold text-black transition-colors hover:bg-emerald-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black">
              Book a scoping call <ArrowRight className="h-4 w-4" aria-hidden="true" />
            </a>
            <a href="#" className="inline-flex h-12 items-center rounded-md border border-white/25 bg-black/40 px-6 text-sm font-semibold text-white transition-colors hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black">
              See a sample report
            </a>
          </motion.div>
        </div>

        <motion.div {...rise(0.3)} className="rounded-xl border border-emerald-400/25 bg-black/75 font-mono shadow-[0_0_60px_-15px_rgba(34,255,136,0.45)]">
          <div className="flex items-center justify-between border-b border-emerald-400/20 px-4 py-3 text-xs text-emerald-200/80">
            <span>report_draft.md</span>
            <span>3 of 17 findings</span>
          </div>
          <ul className="divide-y divide-emerald-400/10">
            {findings.map((f) => (
              <li key={f.id} className="flex items-start gap-3 px-4 py-4 text-sm">
                <span className="mt-0.5 shrink-0 text-emerald-400">{f.id}</span>
                <span className="flex-1 leading-snug text-emerald-50">{f.text}</span>
                <span className={`shrink-0 rounded px-2 py-0.5 text-xs font-semibold ${f.level === "High" ? "bg-rose-500/20 text-rose-200" : "bg-emerald-400/15 text-emerald-200"}`}>{f.level}</span>
              </li>
            ))}
          </ul>
        </motion.div>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/matrix-code-rain-canvas-background.json

When to use it

How to customize it

Accessibility

Questions

How are the fading trails drawn?

Nothing is erased. Each frame paints a nearly transparent black rectangle over the whole canvas and then draws one new character per column. Older characters get covered a little more every frame, so they fade out by themselves.

Why is the first character in each column brighter?

The newest character is drawn in a near white colour, and on the next frame the same character is redrawn in green one row up. That one bright glyph per column is what makes the rain look lit from the front.

Is it heavy on the processor?

No. It draws one or two characters per column about 24 times a second, which is well under a millisecond a frame on a laptop. The device pixel ratio is capped at 2 and the loop pauses off screen.

Why does it look full straight away?

On start and on resize the component runs 70 steps of the simulation before showing anything, and columns start at random heights. Without that, every column would begin at the top and the screen would fill slowly.

Can I put it behind a whole page?

Yes, but give the canvas a fixed-height wrapper or make it `position: fixed` in your own layout. Very tall canvases cost more memory, so a hero-sized area is the better use.

Adapted from Hacker Background in Eldora UI, used under its open-source licence. Licences and credits

Related: Flickering Pixel Grid Canvas Background, Particle Network Background with Connecting Lines, Animated Terminal Window with Typed Commands.

More animated backgrounds or all free UI components. Start for free to have a full site built around it.

More in Backgrounds

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap