Backgrounds
PixelRain
Square pixels fall down a grid and leave fading trails. Pauses off screen.
Download
- React
- Tailwind CSS v4
components/ui/pixel-rain.tsx
"use client";
import { useEffect, useRef, type ReactNode } from "react";
type PixelRainProps = {
children?: ReactNode;
className?: string;
color?: string;
/** Base colour. Also used to fade the trails. */
background?: string;
/** Pixel size in pixels. */
size?: number;
/** Share of columns raining at once, 0 to 1. */
density?: number;
/** 1 is the default pace; 2 is twice as fast. */
speed?: number;
};
type Drop = { y: number; v: number; on: boolean };
/* PixelRain: square pixels fall down a grid and leave fading trails. */
export function PixelRain({
children,
className = "",
color = "#a78bfa",
background = "#05040b",
size = 10,
density = 0.6,
speed = 1,
}: PixelRainProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) return;
const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let w = 0;
let h = 0;
let raf = 0;
let last = 0;
let drops: Drop[] = [];
const newDrop = (y: number): Drop => ({ y, v: 8 + Math.random() * 14, on: Math.random() < density });
const cell = (col: number, row: number) => ctx.fillRect(col * size + 1, row * size + 1, size - 2, size - 2);
// Still frame: each raining column gets a short fixed trail.
const drawStill = () => {
ctx.globalAlpha = 1;
ctx.fillStyle = background;
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = color;
drops.forEach((d, col) => {
if (!d.on) return;
for (let k = 0; k < 8; k++) {
ctx.globalAlpha = (1 - k / 8) * 0.8;
cell(col, Math.floor(d.y) - k);
}
});
ctx.globalAlpha = 1;
};
const frame = (now: number) => {
const dt = last ? Math.min((now - last) / 1000, 0.05) * speed : 0;
last = now;
ctx.globalAlpha = 1 - Math.pow(0.9, dt * 60);
ctx.fillStyle = background;
ctx.fillRect(0, 0, w, h);
ctx.globalAlpha = 1;
ctx.fillStyle = color;
drops.forEach((d, col) => {
d.y += d.v * dt;
if (d.y * size > h + size * 10) drops[col] = newDrop(-Math.random() * 20);
else if (d.on) cell(col, Math.floor(d.y));
});
raf = requestAnimationFrame(frame);
};
const start = () => {
if (still || raf) return;
last = 0;
raf = requestAnimationFrame(frame);
};
const stop = () => {
cancelAnimationFrame(raf);
raf = 0;
};
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);
drops = Array.from({ length: Math.ceil(w / size) }, () => newDrop(Math.random() * (h / size)));
drawStill();
};
// Redraw on size change; only animate while on screen.
const ro = new ResizeObserver(resize);
const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop()));
ro.observe(canvas);
io.observe(canvas);
return () => {
stop();
ro.disconnect();
io.disconnect();
};
}, [color, background, size, density, speed]);
return (
<div className={`relative overflow-hidden ${className}`} style={{ background }}>
<canvas ref={canvasRef} aria-hidden className="absolute inset-0 h-full w-full" />
<div className="relative h-full">{children}</div>
</div>
);
}
1. Paste the Code tab into
components/ui/pixel-rain.tsx. The styles are Tailwind CSS v4 classes.2. Use it. This is the preview above:
app/pixel-rain-demo.tsx
import { PixelRain } from "@/components/ui/pixel-rain"; export default function PixelRainExample() { return ( <PixelRain className="h-full"> <p className="absolute bottom-5 left-6 text-2xl font-medium text-white/90">Falling bits</p> </PixelRain> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Content shown over the rain. |
| color | string | "#a78bfa" | Pixel colour. |
| background | string | "#05040b" | Base colour; also fades the trails. |
| size | number | 10 | Pixel size in pixels. |
| density | number | 0.6 | Share of columns raining at once, 0 to 1. |
| speed | number | 1 | 1 is the default pace; 2 is twice as fast. |
| className | string | "" | Give it a height here. |