---
name: matrix-rain-effect
description: Use when you want "Cyber, nostalgic, exotic" - Displays falling characters inspired by hacker/cyber visuals.
---

# Matrix Rain Effect

> **Category:** Premium / Futuristic  -  **Personality:** Cyber, nostalgic, exotic
>
> **Best use:** Cybersecurity demos, gaming
>
> **Ratings:** Professional 1/5 - Casual 3/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 3/5 - Performance cost 3/5

## What it is

The Matrix "digital rain": columns of glowing katakana, digits and symbols streaming down a black canvas, each glyph decaying into a green trail behind a near-white leading "head". It is drawn with the canvas 2D API — one falling "drop" per column — and evokes the hacker/terminal aesthetic of *The Matrix*. Reach for it on cybersecurity, gaming or sci-fi heroes where a live, ambient, slightly menacing backdrop sells the theme without stealing focus from the copy layered over it.

## Dependencies / CDN

None - vanilla JS + the canvas 2D API; no libraries. The demo only pulls two Google Fonts (display + mono) for the UI that sits on top of the rain — optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@500;700;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
```

## HTML

A single `<canvas>` fills the stage; your real content sits in a layer above it. The scrim (legibility gradient) and scanlines are optional polish:

```html
<section class="mx-stage" aria-label="Matrix digital-rain hero">
  <canvas id="mx-canvas" class="mx-canvas" aria-hidden="true"></canvas>
  <div class="mx-scrim" aria-hidden="true"></div>   <!-- darken + vignette for text -->
  <div class="mx-scan"  aria-hidden="true"></div>   <!-- CRT scanlines -->
  <div class="mx-content">
    <!-- nav, headline, CTAs, stats… everything renders ABOVE the rain -->
  </div>
</section>
```

## CSS

The canvas is absolutely positioned behind content; `overflow:hidden` + `isolation:isolate` keep the glow contained inside the rounded stage:

```css
.mx-stage{
  --mx-ink:#030806;
  position:relative; border-radius:24px; overflow:hidden; isolation:isolate;
  min-height:664px; display:flex; align-items:center;
  background:radial-gradient(120% 85% at 82% -10%,rgba(11,191,99,.12),transparent 58%),var(--mx-ink);
  border:1px solid rgba(43,255,149,.16);
}
.mx-canvas{position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0}

/* darken the left so overlaid copy stays legible, plus a vignette */
.mx-scrim{position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    linear-gradient(90deg,rgba(2,6,5,.94) 0%,rgba(2,6,5,.7) 40%,rgba(2,6,5,.32) 66%,rgba(2,6,5,.6) 100%),
    radial-gradient(135% 120% at 50% 46%,transparent 52%,rgba(0,0,0,.66))}
/* faint CRT scanlines over everything */
.mx-scan{position:absolute; inset:0; z-index:1; pointer-events:none; opacity:.42;
  background:repeating-linear-gradient(0deg,rgba(0,0,0,0) 0 2px,rgba(0,0,0,.55) 2px 3px)}

.mx-content{position:relative; z-index:2}

/* the rain freezes for reduced-motion (handled in JS); kill UI animations too */
@media (prefers-reduced-motion: reduce){ .mx-pulse, .mx-cursor{ animation:none } }
```

## JavaScript

The whole effect. DPR-aware, throttled to 30fps, pauses off-screen, and paints a single frozen frame when reduced-motion is requested:

```js
/* Matrix digital rain — canvas 2D, DPR-aware, off-screen pause, reduced-motion = frozen still */
(function(){
  var stage=document.querySelector('.mx-stage');
  var canvas=document.getElementById('mx-canvas');
  if(!stage||!canvas||!canvas.getContext)return;
  var ctx=canvas.getContext('2d',{alpha:false});
  var reduce=window.matchMedia&&matchMedia('(prefers-reduced-motion: reduce)').matches;

  /* half/full-width katakana + digits + symbols — the classic rain alphabet */
  var GLYPHS=('アァカサタナハマヤャラワガザダバパイィキシチニヒミリギジヂビピウゥクスツヌフムユュルグズエケセテネヘメレゲゼオコソトノホモヨロヲゴゾドボポヴッンｱｲｳｴｵｶｷｸｹｺﾊﾋﾌﾍﾎﾐﾑﾒ0123456789:.=*+<>#%').split('');
  function g(){return GLYPHS[(Math.random()*GLYPHS.length)|0];}

  var dpr=1,W=0,H=0,fontSize=18,cols=0,drops=[],speeds=[],glow=true;

  function setup(){
    dpr=Math.min(window.devicePixelRatio||1,2);
    var r=stage.getBoundingClientRect();
    W=Math.max(1,Math.round(r.width));H=Math.max(1,Math.round(r.height));
    canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);
    canvas.style.width=W+'px';canvas.style.height=H+'px';
    ctx.setTransform(dpr,0,0,dpr,0,0);
    fontSize=W<560?14:18;glow=W>=560;
    cols=Math.ceil(W/fontSize);drops=new Array(cols);speeds=new Array(cols);
    for(var i=0;i<cols;i++){drops[i]=Math.random()*-(H/fontSize);speeds[i]=0.42+Math.random()*0.55;}
    ctx.fillStyle='#030806';ctx.fillRect(0,0,W,H);ctx.textBaseline='top';
  }

  function frame(){
    ctx.fillStyle='rgba(3,8,6,0.085)';ctx.fillRect(0,0,W,H);            /* fade -> trail decay */
    ctx.font=fontSize+"px ui-monospace,'Courier New',monospace";
    for(var i=0;i<cols;i++){
      var x=i*fontSize,y=Math.floor(drops[i])*fontSize;
      ctx.shadowBlur=0;ctx.fillStyle='#1fe87c';ctx.fillText(g(),x,y-fontSize);  /* bright green behind */
      if(glow){ctx.shadowColor='rgba(43,255,149,.9)';ctx.shadowBlur=10;}
      ctx.fillStyle='#daffe9';ctx.fillText(g(),x,y);                   /* near-white glowing head */
      ctx.shadowBlur=0;
      if(y>H&&Math.random()>0.975){drops[i]=0;speeds[i]=0.42+Math.random()*0.55;}
      drops[i]+=speeds[i];
    }
  }

  var raf=0,last=0,INT=1000/30,running=false;
  function loop(t){raf=requestAnimationFrame(loop);if(t-last<INT)return;last=t;frame();}
  function start(){if(running||reduce)return;running=true;last=0;raf=requestAnimationFrame(loop);}
  function stop(){running=false;if(raf)cancelAnimationFrame(raf);raf=0;}

  function staticFrame(){                                              /* reduced-motion: frozen rain */
    ctx.fillStyle='#030806';ctx.fillRect(0,0,W,H);
    ctx.font=fontSize+"px ui-monospace,'Courier New',monospace";ctx.textBaseline='top';
    var rows=Math.ceil(H/fontSize);
    for(var i=0;i<cols;i++){
      var head=2+Math.floor(Math.random()*rows*0.85),len=6+Math.floor(Math.random()*16);
      for(var k=0;k<len;k++){var rIdx=head-k;if(rIdx<0)break;
        ctx.fillStyle='rgba(31,232,124,'+(Math.max(0,1-k/len)*0.85).toFixed(3)+')';
        ctx.fillText(g(),i*fontSize,rIdx*fontSize);}
      ctx.fillStyle='#daffe9';ctx.fillText(g(),i*fontSize,head*fontSize);
    }
  }

  setup();
  if(reduce){staticFrame();}
  else{
    if('IntersectionObserver' in window){
      new IntersectionObserver(function(es){es.forEach(function(e){e.isIntersecting?start():stop();});},{threshold:0.05}).observe(stage);
    }else{start();}
    document.addEventListener('visibilitychange',function(){document.hidden?stop():start();});
  }

  var rt;function onResize(){clearTimeout(rt);rt=setTimeout(function(){setup();if(reduce)staticFrame();},160);}
  if('ResizeObserver' in window){new ResizeObserver(onResize).observe(stage);}else{window.addEventListener('resize',onResize);}
})();
```

## How it works

- **One "drop" per column.** The stage width is divided into columns `fontSize` px wide; `drops[i]` is the current row (a float) of column *i*, advancing by a per-column `speeds[i]` each frame so columns fall at slightly different rates.
- **The trail is a fade, not stored history.** Every frame first paints a near-black rectangle at *low alpha* (`rgba(3,8,6,0.085)`) over the whole canvas, then draws only the new heads. Old glyphs are never redrawn — they simply decay toward black, and that incomplete erase is what produces the green tail.
- **Two-tone glyphs.** Each column draws a bright-green character one row *up* and a near-white, shadow-blurred character at the head, so the leading character reads as a glowing tip with a dimmer body behind it.
- **Reset is probabilistic.** Once a drop passes the bottom (`y>H`) it resets with only ~2.5% chance per frame (`Math.random()>0.975`), so columns restart staggered instead of snapping back in unison.
- **DPR-aware.** `setup()` sizes the backing store to `W*dpr` (capped at 2) and `setTransform(dpr,…)` so glyphs stay crisp on retina without paying for 3× work on high-density phones.
- **rAF, gated to 30fps** via a timestamp check (`t-last<INT`) — smooth enough for rain at roughly half the GPU cost of 60fps.

## Customization

- **Alphabet:** edit the `GLYPHS` string — swap katakana for binary `01`, hex, runes, or your brand's charset.
- **Colour:** change the head (`#daffe9`), the trailing green (`#1fe87c`) and the glow (`shadowColor`) together to recolour the whole rain (amber, cyan, red).
- **Density / size:** `fontSize` controls column count — smaller = more, denser columns.
- **Speed:** the `0.42+Math.random()*0.55` range sets fall speed per column; raise both numbers for a downpour.
- **Trail length:** the fade alpha — lower (`0.05`) = longer tails, higher (`0.15`) = short, snappy drops.
- **Frame rate:** `INT=1000/30`; use `1000/60` for buttery motion or `1000/24` to save battery.

## Accessibility & performance

- **Decorative:** the canvas is `aria-hidden="true"` so screen readers skip it; all meaningful content lives in `.mx-content` above it.
- **Reduced motion:** when `prefers-reduced-motion: reduce` is set the loop never starts — `staticFrame()` paints a single frozen rain still instead, so the theme survives without animation.
- **Off-screen pause:** an `IntersectionObserver` stops the loop when the stage scrolls out of view, and `visibilitychange` stops it when the tab is hidden — zero CPU when nothing is visible.
- **Cheap by design:** `getContext('2d',{alpha:false})` (opaque = faster compositing), DPR capped at 2, the 30fps throttle, and glow (`shadowBlur`) disabled below 560px where it costs the most.
- **Resize is debounced** (160ms) and rebuilds the column/speed arrays to match the new size so columns never overflow or leave gaps.

## Gotchas

- **The fade rectangle must be translucent black, never `clearRect`.** Clearing each frame deletes the trail; the entire effect *is* the incomplete erase. The fade alpha (`0.085`) and the stage's true background (`#030806`) must agree or you get muddy banding.
- **`{alpha:false}` means the canvas is never transparent** — you can't see page content *through* the rain, you layer content *over* it. Keep `ctx.fillStyle` black matched to your stage colour.
- **Glyph width vs `fontSize`.** Columns are spaced by `fontSize`, but full-width katakana are roughly square while half-width/ASCII glyphs are narrower, so spacing is approximate — fine for rain, don't rely on a pixel-perfect grid.
- **The DPR transform persists.** `setTransform(dpr,…)` runs on every `setup()`; if you add your own draws, remember coordinates are in CSS pixels, not device pixels.
- **`shadowBlur` is the expensive call.** Leaving glow on for every glyph on a large high-DPR canvas will drop frames — the demo only enables it ≥560px and resets `shadowBlur=0` before the next fill.
