Dynamic Pixel Grid
A canvas grid of pixels that flicker random opacities inside a sine-pulsing circle — a breathing field of static. Themeable, DPR-crisp, reduced-motion aware. Based on Alex Krasikau.
Usage
import { PixelGrid } from "~/components/effects/pixel-grid";<PixelGrid size={360} color="hsl(0 0% 90%)" />// other mask shapes: "square" | "diamond" | "ring" | "full"<PixelGrid shape="diamond" density={0.15} />
How it works
It's a single <canvas> and a loop. The grid is ~100×100 little squares, and each one just stores an opacity. Every few frames, a slice of random cells (about 20%) gets bumped to a random opacity, and the rest hold their value. That alone reads as soft TV static.
The circle is what makes it feel alive. A radius breathes in and out on a Math.sin, and when a cell gets picked, it only lights up if it's inside that radius, otherwise it's snapped to zero. So the field of static swells and contracts. I made it themeable (the original drew black on white; this defaults to light pixels for dark backgrounds), scaled it for device pixel ratio so the squares stay crisp, and had it draw one still frame under prefers-reduced-motion.
// 100x100 cells; each tick, refresh ~20% of them to a random opacity.// Inside the pulsing circle they flicker; outside they go dark.const radius = baseR + amp * Math.sin(frame * 0.01);if (frame % 5 === 0) {const updates = Math.floor(count * density);for (let i = 0; i < updates; i++) {const x = (Math.random() * grid) | 0;const y = (Math.random() * grid) | 0;const dx = x * step + pixelSize / 2 - center;const dy = y * step + pixelSize / 2 - center;op[y * grid + x] =dx * dx + dy * dy <= radius * radius? Math.random() * 0.6 + 0.2: 0;}}
Props
| prop | type | default | description |
|---|---|---|---|
| size | number | 360 | Canvas size in px (square). |
| pixelSize | number | 4 | Pixel side length. |
| gap | number | 4 | Gap between pixels. |
| color | string | "hsl(0 0% 90%)" | Any CSS color. |
| density | number | 0.2 | Fraction of pixels refreshed each tick. |
| shape | "circle" | "square" | "diamond" | "ring" | "full" | "circle" | Mask the flicker to a sine-pulsing shape. |
Original by Alex Krasikau.
Source
download .zipThe full implementation. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).
import { useEffect, useRef } from "react";import { cn } from "~/lib/utils";export interface PixelGridProps {/** Canvas size in px (square). Default 360. */size?: number;/** Pixel side length in px. Default 4. */pixelSize?: number;/** Gap between pixels in px. Default 4. */gap?: number;/** Pixel color (any CSS color). Default a light grey for dark backgrounds. */color?: string;/** Fraction of pixels refreshed each tick (0–1). Default 0.2. */density?: number;/** Mask the flicker to a sine-pulsing shape. Default "circle". */shape?: "circle" | "square" | "diamond" | "ring" | "full";/** Deprecated alias: `circle={false}` is the same as `shape="full"`. */circle?: boolean;className?: string;}/*** A canvas grid of pixels that flicker random opacities, optionally confined to* a circle whose radius breathes with a sine wave. DPR-aware and crisp; honors* prefers-reduced-motion (draws one static frame). Based on Alex Krasikau's* dynamic pixel grid.*/export function PixelGrid({size = 360,pixelSize = 4,gap = 4,color = "hsl(0 0% 90%)",density = 0.2,shape: shapeProp,circle,className,}: PixelGridProps) {const shape = shapeProp ?? (circle === false ? "full" : "circle");const ref = useRef<HTMLCanvasElement>(null);useEffect(() => {const canvas = ref.current;if (!canvas) return;const ctx = canvas.getContext("2d");if (!ctx) return;const dpr = Math.min(window.devicePixelRatio || 1, 2);canvas.width = size * dpr;canvas.height = size * dpr;ctx.scale(dpr, dpr);const step = pixelSize + gap;const grid = Math.floor(size / step);const count = grid * grid;const op = new Float32Array(count);const center = size / 2;const minR = size * 0.2;const maxR = size * 0.5;const amp = (maxR - minR) / 2;const baseR = minR + amp;const draw = () => {ctx.clearRect(0, 0, size, size);ctx.fillStyle = color;for (let y = 0; y < grid; y++) {for (let x = 0; x < grid; x++) {const a = op[y * grid + x];if (a <= 0) continue;ctx.globalAlpha = a;ctx.fillRect(x * step, y * step, pixelSize, pixelSize);}}ctx.globalAlpha = 1;};const band = size * 0.06;const inside = (dx: number, dy: number, r: number) => {switch (shape) {case "full":return true;case "square":return Math.max(Math.abs(dx), Math.abs(dy)) <= r;case "diamond":return Math.abs(dx) + Math.abs(dy) <= r;case "ring":return Math.abs(Math.hypot(dx, dy) - r) <= band;default:return dx * dx + dy * dy <= r * r;}};const refresh = (radius: number) => {const updates = Math.floor(count * density);for (let i = 0; i < updates; i++) {const x = Math.floor(Math.random() * grid);const y = Math.floor(Math.random() * grid);const idx = y * grid + x;const dx = x * step + pixelSize / 2 - center;const dy = y * step + pixelSize / 2 - center;op[idx] = inside(dx, dy, radius)? Math.random() * 0.6 + 0.2: 0;}};const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;if (reduced) {refresh(baseR);draw();return;}let raf = 0;let frame = 0;const animate = () => {frame++;const radius = baseR + amp * Math.sin(frame * 0.01);if (frame % 5 === 0) refresh(radius);draw();raf = requestAnimationFrame(animate);};animate();return () => cancelAnimationFrame(raf);}, [size, pixelSize, gap, color, density, shape]);return (<canvasref={ref}role="img"aria-label="Animated pixel grid"className={cn(className)}style={{ width: size, height: size, maxWidth: "100%" }}/>);}