An animated grid of subtly shifting glyphs on a canvas
1"use client";2
3import { GlyphMatrix } from "@/components/backgrounds/pxl/glyph-matrix";4
5export default function GlyphMatrixDemo() {6 return (7 <GlyphMatrix />8 );9}Installation
Section titled “Installation”pnpm dlx shadcn@latest add pxl-ui/registry/backgrounds/glyph-matrix
Copy and paste the following code into your project.
1"use client";2
3import { cva, type VariantProps } from "class-variance-authority";4import { type HTMLAttributes, useEffect, useLayoutEffect, useRef, useState } from "react";5
6import { cn } from "@/lib/utils";7
8const glyphMatrixVariants = cva("absolute inset-0 z-0 size-full", {9 variants: {10 font: {11 default: "font-mono",12 mono: "font-mono",13 heading: "font-heading",14 sans: "font-sans",15 },16 variant: {17 default: "bg-background text-foreground",18 primary: "bg-primary text-primary-foreground",19 secondary: "bg-secondary text-secondary-foreground",20 info: "bg-info text-info-foreground",21 success: "bg-success text-success-foreground",22 warning: "bg-warning text-warning-foreground",23 danger: "bg-danger text-danger-foreground",24 muted: "bg-muted text-muted-foreground",25 },26 size: {27 default: "text-sm",28 "4xs": "text-4xs",29 "3xs": "text-3xs",30 "2xs": "text-2xs",31 xs: "text-xs",32 sm: "text-sm",33 md: "text-base",34 lg: "text-lg",35 xl: "text-xl",36 "2xl": "text-2xl",37 "3xl": "text-3xl",38 }39 },40});41
42function GlyphMatrix({43 className,44 font = "default",45 glyphs = "01·•+*/\\<>=",46 mutationRate = 0.04,47 interval = 90,48 fadeBottom = 0.6,49 size = "default",50 variant = "default",51 ...props52}: HTMLAttributes<HTMLCanvasElement> & VariantProps<typeof glyphMatrixVariants> & {53 /** Characters to randomly pick from */54 glyphs?: string;55 /** Probability (0-1) a cell mutates each tick */56 mutationRate?: number;57 /** Tick interval in ms */58 interval?: number;59 /** Fade out toward bottom (0 = no fade) */60 fadeBottom?: number;61}) {62 const containerRef = useRef<HTMLDivElement | null>(null);63 const canvasRef = useRef<HTMLCanvasElement | null>(null);64 const stylesRef = useRef({65 // Current glyph color as RGBA (a in 0-1). Kept in a ref so a color change66 // (e.g. theme toggle) recolors the next frame without restarting the67 // animation. Defaults to #6B7280.68 color: { r: 107, g: 114, b: 128, a: 1 },69 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",70 fontSize: 14,71 });72
73 const [computedColor, setComputedColor] = useState("rgba(107, 114, 128, 1)");74
75 // biome-ignore lint/correctness/useExhaustiveDependencies: computed styles might change on font and variant changes76 useLayoutEffect(function resolveComputedStyles() {77 if (!containerRef.current) return;78
79 const computedStyle = getComputedStyle(containerRef.current);80 stylesRef.current.fontFamily = computedStyle.fontFamily;81 stylesRef.current.fontSize = Number.parseFloat(computedStyle.fontSize);82
83 setComputedColor(computedStyle.color);84 }, [font, size, variant]);85
86 // Resolve the CSS color string to RGBA (handles hex, rgb, hsl, oklch, ...).87 useEffect(function resolveRGBATextColor() {88 const probe = document.createElement("canvas");89 probe.width = 1;90 probe.height = 1;91 const probeCtx = probe.getContext("2d");92 if (!probeCtx) return;93 // Seed with the default so an invalid color falls back to it: the 2d94 // context keeps the previous fillStyle when assigned an invalid value95 // instead of silently turning black.96 probeCtx.fillStyle = "#6B7280";97 probeCtx.fillStyle = computedColor;98 probeCtx.fillRect(0, 0, 1, 1);99 const [r, g, b, a] = probeCtx.getImageData(0, 0, 1, 1).data;100 stylesRef.current.color = { r, g, b, a: a / 255 };101 }, [computedColor]);102
103 useEffect(function renderGlyphs() {104 const canvas = canvasRef.current;105 if (!canvas) return;106
107 const ctx = canvas.getContext("2d");108 if (!ctx) return;109
110 let cols = 0;111 let rows = 0;112 let cells: string[] = [];113 let alphas: number[] = [];114 let raf = 0;115 let last = 0;116 let stopped = false;117
118 const resize = () => {119 const dpr = window.devicePixelRatio || 1;120 const { clientWidth: w, clientHeight: h } = canvas;121
122 canvas.width = w * dpr;123 canvas.height = h * dpr;124 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);125
126 cols = Math.ceil(w / stylesRef.current.fontSize);127 rows = Math.ceil(h / stylesRef.current.fontSize);128
129 cells = new Array(cols * rows)130 .fill(0)131 .map(() => glyphs[Math.floor(Math.random() * glyphs.length)]);132 alphas = new Array(cols * rows)133 .fill(0)134 .map(() => 0.05 + Math.random() * 0.35);135 };136
137 const draw = () => {138 const { clientWidth: w, clientHeight: h } = canvas;139 ctx.clearRect(0, 0, w, h);140
141 ctx.font = `${stylesRef.current.fontSize - 2}px ${stylesRef.current.fontFamily}`;142 ctx.textBaseline = "top";143
144 const { r, g, b, a: colorAlpha } = stylesRef.current.color;145 for (let y = 0; y < rows; y++) {146 const fade = fadeBottom > 0 ? 1 - (y / rows) * fadeBottom : 1;147 for (let x = 0; x < cols; x++) {148 const i = y * cols + x;149 const a = alphas[i] * fade * colorAlpha;150 ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;151 ctx.fillText(cells[i], x * stylesRef.current.fontSize, y * stylesRef.current.fontSize);152 }153 }154 };155
156 const tick = (t: number) => {157 if (stopped) return;158
159 if (t - last >= interval) {160 last = t;161
162 const total = cols * rows;163 const mutations = Math.max(1, Math.floor(total * mutationRate));164
165 for (let n = 0; n < mutations; n++) {166 const i = Math.floor(Math.random() * total);167 cells[i] = glyphs[Math.floor(Math.random() * glyphs.length)];168 alphas[i] = 0.05 + Math.random() * 0.45;169 }170
171 draw();172 }173
174 raf = requestAnimationFrame(tick);175 };176
177 resize();178 draw();179 raf = requestAnimationFrame(tick);180
181 const ro = new ResizeObserver(() => {182 resize();183 draw();184 });185 ro.observe(canvas);186
187 return () => {188 stopped = true;189 cancelAnimationFrame(raf);190 ro.disconnect();191 };192 }, [glyphs, mutationRate, interval, fadeBottom]);193
194 return (195 <div className={cn(glyphMatrixVariants({ font, size, variant, }), className)} ref={containerRef}>196 {/** biome-ignore lint/a11y/noAriaHiddenOnFocusable: background canvas */}197 <canvas198 ref={canvasRef}199 className="pointer-events-none w-full h-full block"200 aria-hidden="true"201 {...props}202 />203 </div>204 );205}206
207export { GlyphMatrix };Update the import paths to match your project setup.
import { GlyphMatrix } from "@/components/backgrounds/pxl/glyph-matrix"<GlyphMatrix />Use the size prop to change the size of the glyphs.
1"use client";2
3import { GlyphMatrix } from "@/components/backgrounds/pxl/glyph-matrix";4
5export default function GlyphMatrixSizes() {6 return (7 <GlyphMatrix8 size="lg" />9 );10}Variants
Section titled “Variants”Use the variant prop to change the colors of the matrix.
1"use client";2
3import { GlyphMatrix } from "@/components/backgrounds/pxl/glyph-matrix";4
5export default function GlyphMatrixVariants() {6 return (7 <div className="flex flex-wrap size-full items-center justify-center gap-2">8 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">9 <GlyphMatrix10 glyphs="muted"11 variant="muted"12 />13 </div>14 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">15 <GlyphMatrix16 glyphs="primary"17 variant="primary"18 />19 </div>20 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">21 <GlyphMatrix22 glyphs="info"23 variant="info"24 />25 </div>26 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">27 <GlyphMatrix28 glyphs="success"29 variant="success"30 />31 </div>32 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">33 <GlyphMatrix34 glyphs="warning"35 variant="warning"36 />37 </div>38 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">39 <GlyphMatrix40 glyphs="danger"41 variant="danger"42 />43 </div>44 </div>45 );46}API Reference
Section titled “API Reference”GlyphMatrix
Section titled “GlyphMatrix”| Prop | Type | Default | Description |
|---|---|---|---|
glyphs |
string |
"01·•+*/\\<>=" |
Characters to randomly pick from. |
size |
"default" | "4xs" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" |
"default" |
Size of the glyphs |
variant |
"default" | "primary" | "secondary" | "muted" | "success" | "warning" | "danger" |
"default" |
Colors of the grids |
font |
"default" | "mono" | "heading" | "sans" |
Font family of the glyphs | |
mutationRate |
number |
0.04 |
Probability a cell mutates each tick. |
interval |
number |
90 |
Tick interval in milliseconds. |
className |
string |
- |
Classes applied to the canvas element. |
fadeBottom |
number |
0.6 |
Fade strength toward the bottom of the grid. |