Feature description
Overview
Add a new component called GlyphMatrix to Magic UI.
GlyphMatrix renders an animated grid of glyphs/symbols on a where characters subtly mutate over time, creating a futuristic terminal / cyberpunk / matrix-like effect while remaining lightweight and theme-aware.
This component is especially useful for:
- Hero section backgrounds
- AI / developer portfolios
- Terminal-inspired UIs
- Dashboard ambient effects
- Cyberpunk aesthetics
- Interactive landing pages
Proposed API
interface GlyphMatrixProps {
/** Characters to randomly pick from */
glyphs?: string;
/** Cell size in px (also font size) */
cellSize?: number;
/** Probability (0-1) a cell mutates each tick */
mutationRate?: number;
/** Tick interval in ms */
interval?: number;
/** Optional className for the wrapping canvas */
className?: string;
/** Fade out toward bottom (0 = no fade) */
fadeBottom?: number;
}
Features
- Lightweight canvas-based rendering
- Theme-aware using CSS variables (--foreground)
- Responsive resizing via ResizeObserver
- Smooth animation with requestAnimationFrame
- Configurable glyph set
- Adjustable mutation speed/intensity
- Subtle opacity variation for depth
- Optional bottom fade effect
- Works well in both light and dark themes
Example Usage
import { GlyphMatrix } from "@/components/magicui/glyph-matrix";
export default function Demo() {
return (
<div className="relative h-[500px] w-full overflow-hidden rounded-xl border">
<GlyphMatrix
className="absolute inset-0"
glyphs="01アイウエオ<>[]{}"
cellSize={16}
mutationRate={0.05}
interval={80}
fadeBottom={0.7}
/>
<div className="relative z-10 flex h-full items-center justify-center">
<h1 className="text-4xl font-bold">
Glyph Matrix
</h1>
</div>
</div>
);
}
Expected File Structure
components/magicui/glyph-matrix.tsx
Additional Notes
- Uses for performance instead of DOM nodes
- Avoid external animation dependencies
- Should follow existing Magic UI component conventions
- Could later support:
- directional glyph flow
- glow intensity
- interactive mouse effects
- color gradients
- noise-based motion
JSX Version (Javascript)
"use client";
import { useEffect, useRef } from "react";
export function GlyphMatrix({
glyphs = "01·•+*/\\<>=",
cellSize = 14,
mutationRate = 0.04,
interval = 90,
className,
fadeBottom = 0.6,
}) {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let cols = 0;
let rows = 0;
let cells = [];
let alphas = [];
let raf = 0;
let last = 0;
let stopped = false;
const readColor = () => {
const styles = getComputedStyle(canvas);
const probe = document.createElement("span");
probe.style.color = "var(--foreground)";
probe.style.display = "none";
canvas.parentElement?.appendChild(probe);
const color = getComputedStyle(probe).color || styles.color;
probe.remove();
return color;
};
let fgColor = readColor();
const resize = () => {
const dpr = window.devicePixelRatio || 1;
const { clientWidth: w, clientHeight: h } = canvas;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cols = Math.ceil(w / cellSize);
rows = Math.ceil(h / cellSize);
cells = new Array(cols * rows)
.fill(0)
.map(
() => glyphs[Math.floor(Math.random() * glyphs.length)]
);
alphas = new Array(cols * rows)
.fill(0)
.map(() => 0.05 + Math.random() * 0.35);
fgColor = readColor();
};
const parseRgb = (c) => {
const m = c.match(/rgba?\(([^)]+)\)/);
if (!m) {
return { r: 0, g: 0, b: 0 };
}
const [r, g, b] = m[1]
.split(",")
.map((v) => parseFloat(v));
return { r, g, b };
};
const draw = () => {
const { clientWidth: w, clientHeight: h } = canvas;
ctx.clearRect(0, 0, w, h);
const { r, g, b } = parseRgb(fgColor);
ctx.font = `${
cellSize - 2
}px ui-monospace, SFMono-Regular, Menlo, monospace`;
ctx.textBaseline = "top";
for (let y = 0; y < rows; y++) {
const fade =
fadeBottom > 0
? 1 - (y / rows) * fadeBottom
: 1;
for (let x = 0; x < cols; x++) {
const i = y * cols + x;
const a = alphas[i] * fade;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
ctx.fillText(
cells[i],
x * cellSize,
y * cellSize
);
}
}
};
const tick = (t) => {
if (stopped) return;
if (t - last >= interval) {
last = t;
const total = cols * rows;
const mutations = Math.max(
1,
Math.floor(total * mutationRate)
);
for (let n = 0; n < mutations; n++) {
const i = Math.floor(Math.random() * total);
cells[i] =
glyphs[
Math.floor(Math.random() * glyphs.length)
];
alphas[i] = 0.05 + Math.random() * 0.45;
}
draw();
}
raf = requestAnimationFrame(tick);
};
resize();
draw();
raf = requestAnimationFrame(tick);
const ro = new ResizeObserver(() => {
resize();
draw();
});
ro.observe(canvas);
const mo = new MutationObserver(() => {
fgColor = readColor();
});
mo.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "data-theme"],
});
return () => {
stopped = true;
cancelAnimationFrame(raf);
ro.disconnect();
mo.disconnect();
};
}, [
glyphs,
cellSize,
mutationRate,
interval,
fadeBottom,
]);
return (
<canvas
ref={canvasRef}
className={className}
style={{
width: "100%",
height: "100%",
display: "block",
}}
aria-hidden="true"
/>
);
}
export default GlyphMatrix;
TSX Version (Typescript)
"use client"
import { useEffect, useRef } from "react"
interface GlyphMatrixProps {
/** Characters to randomly pick from */
glyphs?: string
/** Cell size in px (also font size) */
cellSize?: number
/** Probability (0-1) a cell mutates each tick */
mutationRate?: number
/** Tick interval in ms */
interval?: number
/** Optional className for the wrapping canvas */
className?: string
/** Fade out toward bottom (0 = no fade) */
fadeBottom?: number
}
/**
* GlyphMatrix — an animated grid of subtly shifting glyphs.
* Uses semantic tokens (--foreground / --background) so it adapts to
* both light and dark modes automatically.
*/
export function GlyphMatrix({
glyphs = "01·•+*/\\<>=",
cellSize = 14,
mutationRate = 0.04,
interval = 90,
className = "text-black dark:text-white",
fadeBottom = 0.6,
}: GlyphMatrixProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
let cols = 0
let rows = 0
let cells: string[] = []
let alphas: number[] = []
let raf = 0
let last = 0
let stopped = false
const readColor = () => {
const styles = getComputedStyle(canvas)
// Resolve --foreground via a temp element so oklch() is converted to rgb
const probe = document.createElement("span")
probe.style.color = "var(--foreground)"
probe.style.display = "none"
canvas.parentElement?.appendChild(probe)
const color = getComputedStyle(probe).color || styles.color
probe.remove()
return color
}
let fgColor = readColor()
const resize = () => {
const dpr = window.devicePixelRatio || 1
const { clientWidth: w, clientHeight: h } = canvas
canvas.width = w * dpr
canvas.height = h * dpr
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
cols = Math.ceil(w / cellSize)
rows = Math.ceil(h / cellSize)
cells = new Array(cols * rows)
.fill(0)
.map(() => glyphs[Math.floor(Math.random() * glyphs.length)])
alphas = new Array(cols * rows)
.fill(0)
.map(() => 0.05 + Math.random() * 0.35)
fgColor = readColor()
}
const parseRgb = (c: string) => {
const m = c.match(/rgba?\(([^)]+)\)/)
if (!m) return { r: 0, g: 0, b: 0 }
const [r, g, b] = m[1].split(",").map((v) => parseFloat(v))
return { r, g, b }
}
const draw = () => {
const { clientWidth: w, clientHeight: h } = canvas
ctx.clearRect(0, 0, w, h)
const { r, g, b } = parseRgb(fgColor)
ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`
ctx.textBaseline = "top"
for (let y = 0; y < rows; y++) {
const fade = fadeBottom > 0 ? 1 - (y / rows) * fadeBottom : 1
for (let x = 0; x < cols; x++) {
const i = y * cols + x
const a = alphas[i] * fade
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`
ctx.fillText(cells[i], x * cellSize, y * cellSize)
}
}
}
const tick = (t: number) => {
if (stopped) return
if (t - last >= interval) {
last = t
const total = cols * rows
const mutations = Math.max(1, Math.floor(total * mutationRate))
for (let n = 0; n < mutations; n++) {
const i = Math.floor(Math.random() * total)
cells[i] = glyphs[Math.floor(Math.random() * glyphs.length)]
alphas[i] = 0.05 + Math.random() * 0.45
}
draw()
}
raf = requestAnimationFrame(tick)
}
resize()
draw()
raf = requestAnimationFrame(tick)
const ro = new ResizeObserver(() => {
resize()
draw()
})
ro.observe(canvas)
// Re-read color when theme changes (class on <html>)
const mo = new MutationObserver(() => {
fgColor = readColor()
})
mo.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "data-theme"],
})
return () => {
stopped = true
cancelAnimationFrame(raf)
ro.disconnect()
mo.disconnect()
}
}, [glyphs, cellSize, mutationRate, interval, fadeBottom])
return (
<canvas
ref={canvasRef}
className={className}
style={{ width: "100%", height: "100%", display: "block" }}
aria-hidden="true"
/>
)
}
export default GlyphMatrix
Affected component/components
None.
Additional Context
It is already implemented in my portfolio at www.sarvankumar.in.
Screenshot
Before submitting
Feature description
Overview
Add a new component called GlyphMatrix to Magic UI.
GlyphMatrix renders an animated grid of glyphs/symbols on a where characters subtly mutate over time, creating a futuristic terminal / cyberpunk / matrix-like effect while remaining lightweight and theme-aware.
This component is especially useful for:
Proposed API
Features
Example Usage
Expected File Structure
Additional Notes
JSX Version (Javascript)
TSX Version (Typescript)
Affected component/components
None.
Additional Context
It is already implemented in my portfolio at www.sarvankumar.in.
Screenshot
Before submitting