diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts
index 92df27973..19e1ea203 100644
--- a/apps/www/config/docs.ts
+++ b/apps/www/config/docs.ts
@@ -651,6 +651,12 @@ export const docsConfig: DocsConfig = {
items: [],
label: "New",
},
+ {
+ title: "Glyph Matrix",
+ href: `/docs/components/glyph-matrix`,
+ items: [],
+ label: "New",
+ },
],
},
],
diff --git a/apps/www/content/docs/components/glyph-matrix.mdx b/apps/www/content/docs/components/glyph-matrix.mdx
new file mode 100644
index 000000000..add8566ad
--- /dev/null
+++ b/apps/www/content/docs/components/glyph-matrix.mdx
@@ -0,0 +1,68 @@
+---
+title: Glyph Matrix
+date: 2026-06-12
+description: An animated grid of subtly shifting glyphs that reads CSS theme tokens directly, so it works in both light and dark mode.
+author: magicui
+published: true
+---
+
+
+
+## Installation
+
+
+
+
+ CLI
+ Manual
+
+
+
+```bash
+npx shadcn@latest add @magicui/glyph-matrix
+```
+
+
+
+
+
+
+
+Copy and paste the following code into your project.
+
+
+
+Update the import paths to match your project setup.
+
+
+
+
+
+
+
+## Usage
+
+```tsx showLineNumbers
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+```
+
+```tsx showLineNumbers
+
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| -------------- | -------- | ---------------- | -------------------------------------------- |
+| `glyphs` | `string` | `"01·•+*/\\<>="` | Characters to randomly pick from. |
+| `cellSize` | `number` | `14` | Cell size in pixels and font size. |
+| `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. |
+
+## Notes
+
+The component resolves `--foreground` through the browser and normalizes the computed CSS color before drawing. That means Tailwind v4 and shadcn tokens that serialize as `oklch(...)` still render correctly on canvas.
diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt
index ec3bbf83f..231bf5db0 100644
--- a/apps/www/public/llms-full.txt
+++ b/apps/www/public/llms-full.txt
@@ -8829,6 +8829,215 @@ export default function GlobeDemo() {
+===== COMPONENT: glyph-matrix =====
+Title: Glyph Matrix
+Description: An animated grid of subtly shifting glyphs with fade effect and theme support.
+
+--- file: magicui/glyph-matrix.tsx ---
+"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,
+ fadeBottom = 0.6,
+}: GlyphMatrixProps) {
+ 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: string[] = []
+ let alphas: number[] = []
+ let raf = 0
+ let last = 0
+ let stopped = false
+
+ const colorCanvas = document.createElement("canvas")
+ colorCanvas.width = 1
+ colorCanvas.height = 1
+ const colorContext = colorCanvas.getContext("2d")
+
+ const readColor = () => {
+ const probe = document.createElement("span")
+ probe.style.color = "var(--foreground)"
+ probe.style.display = "none"
+ document.body.appendChild(probe)
+ const computed = getComputedStyle(probe).color
+ probe.remove()
+ return computed
+ }
+
+ const parseColor = (value: string) => {
+ if (!colorContext) return { r: 0, g: 0, b: 0 }
+
+ colorContext.fillStyle = "#000"
+ colorContext.fillStyle = value
+ const normalized = colorContext.fillStyle
+ colorContext.fillStyle = normalized
+ colorContext.fillRect(0, 0, 1, 1)
+ const pixels = colorContext.getImageData(0, 0, 1, 1).data
+ const r = pixels[0]
+ const g = pixels[1]
+ const b = pixels[2]
+
+ return { r, g, b }
+ }
+
+ 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 draw = () => {
+ const { clientWidth: w, clientHeight: h } = canvas
+ ctx.clearRect(0, 0, w, h)
+
+ const { r, g, b } = parseColor(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 )
+ const mo = new MutationObserver(() => {
+ fgColor = readColor()
+ draw()
+ })
+ 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 (
+
+ )
+}
+
+
+===== EXAMPLE: glyph-matrix-demo =====
+Title: Glyph Matrix Demo
+
+--- file: example/glyph-matrix-demo.tsx ---
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+
+export default function GlyphMatrixDemo() {
+ return (
+
+
+
+ )
+}
+
+
+
===== COMPONENT: grid-pattern =====
Title: Grid Pattern
Description: A background grid pattern made with SVGs, fully customizable using Tailwind CSS.
diff --git a/apps/www/public/llms.txt b/apps/www/public/llms.txt
index dd7e1290c..a375dac2d 100644
--- a/apps/www/public/llms.txt
+++ b/apps/www/public/llms.txt
@@ -33,6 +33,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Flickering Grid](https://magicui.design/docs/components/flickering-grid): A flickering grid background made with SVGs, fully customizable using Tailwind CSS.
- [Glare Hover](https://magicui.design/docs/components/glare-hover): A diagonal glare on hover using a ::before gradient and CSS variables (angle, size, duration, color).
- [Globe](https://magicui.design/docs/components/globe): An autorotating, interactive, and highly performant globe made using WebGL.
+- [Glyph Matrix](https://magicui.design/docs/components/glyph-matrix): An animated grid of subtly shifting glyphs with fade effect and theme support.
- [Grid Pattern](https://magicui.design/docs/components/grid-pattern): A background grid pattern made with SVGs, fully customizable using Tailwind CSS.
- [Hero Video Dialog](https://magicui.design/docs/components/hero-video-dialog): A hero video dialog component.
- [Hexagon Pattern](https://magicui.design/docs/components/hexagon-pattern): A background hexagon pattern made with SVGs, fully customizable using Tailwind CSS.
@@ -130,6 +131,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Marquee Logos](https://github.com/magicuidesign/magicui/blob/main/example/marquee-logos.tsx): Example usage
- [Marquee 3D](https://github.com/magicuidesign/magicui/blob/main/example/marquee-3d.tsx): Example usage
- [Globe Demo](https://github.com/magicuidesign/magicui/blob/main/example/globe-demo.tsx): Example usage
+- [Glyph Matrix Demo](https://github.com/magicuidesign/magicui/blob/main/example/glyph-matrix-demo.tsx): Example usage
- [Glare Hover Demo](https://github.com/magicuidesign/magicui/blob/main/example/glare-hover-demo.tsx): Example usage
- [Glare Hover Demo — CTA](https://github.com/magicuidesign/magicui/blob/main/example/glare-hover-demo-cta.tsx): Example usage
- [Glare Hover Demo — Alerts](https://github.com/magicuidesign/magicui/blob/main/example/glare-hover-demo-alert.tsx): Example usage
diff --git a/apps/www/public/r/glyph-matrix-demo.json b/apps/www/public/r/glyph-matrix-demo.json
new file mode 100644
index 000000000..3bba19050
--- /dev/null
+++ b/apps/www/public/r/glyph-matrix-demo.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "content": "import { GlyphMatrix } from \"@/registry/magicui/glyph-matrix\"\n\nexport default function GlyphMatrixDemo() {\n return (\n \n =\"\n cellSize={14}\n mutationRate={0.04}\n interval={90}\n fadeBottom={0.6}\n />\n
\n )\n}\n",
+ "type": "registry:example"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/www/public/r/glyph-matrix.json b/apps/www/public/r/glyph-matrix.json
new file mode 100644
index 000000000..dd5b43809
--- /dev/null
+++ b/apps/www/public/r/glyph-matrix.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "glyph-matrix",
+ "type": "registry:ui",
+ "title": "Glyph Matrix",
+ "description": "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ "files": [
+ {
+ "path": "registry/magicui/glyph-matrix.tsx",
+ "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface GlyphMatrixProps {\n /** Characters to randomly pick from */\n glyphs?: string\n /** Cell size in px (also font size) */\n cellSize?: number\n /** Probability (0-1) a cell mutates each tick */\n mutationRate?: number\n /** Tick interval in ms */\n interval?: number\n /** Optional className for the wrapping canvas */\n className?: string\n /** Fade out toward bottom (0 = no fade) */\n fadeBottom?: number\n}\n\n/**\n * GlyphMatrix — an animated grid of subtly shifting glyphs.\n * Uses semantic tokens (--foreground / --background) so it adapts to\n * both light and dark modes automatically.\n */\nexport function GlyphMatrix({\n glyphs = \"01·•+*/\\\\<>=\",\n cellSize = 14,\n mutationRate = 0.04,\n interval = 90,\n className,\n fadeBottom = 0.6,\n}: GlyphMatrixProps) {\n const canvasRef = useRef(null)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) return\n\n let cols = 0\n let rows = 0\n let cells: string[] = []\n let alphas: number[] = []\n let raf = 0\n let last = 0\n let stopped = false\n\n const colorCanvas = document.createElement(\"canvas\")\n colorCanvas.width = 1\n colorCanvas.height = 1\n const colorContext = colorCanvas.getContext(\"2d\")\n\n const readColor = () => {\n const probe = document.createElement(\"span\")\n probe.style.color = \"var(--foreground)\"\n probe.style.display = \"none\"\n document.body.appendChild(probe)\n const computed = getComputedStyle(probe).color\n probe.remove()\n return computed\n }\n\n const parseColor = (value: string) => {\n if (!colorContext) return { r: 0, g: 0, b: 0 }\n\n colorContext.fillStyle = \"#000\"\n colorContext.fillStyle = value\n const normalized = colorContext.fillStyle\n colorContext.fillStyle = normalized\n colorContext.fillRect(0, 0, 1, 1)\n const pixels = colorContext.getImageData(0, 0, 1, 1).data\n const r = pixels[0]\n const g = pixels[1]\n const b = pixels[2]\n\n return { r, g, b }\n }\n\n let fgColor = readColor()\n\n const resize = () => {\n const dpr = window.devicePixelRatio || 1\n const { clientWidth: w, clientHeight: h } = canvas\n\n canvas.width = w * dpr\n canvas.height = h * dpr\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n\n cols = Math.ceil(w / cellSize)\n rows = Math.ceil(h / cellSize)\n\n cells = new Array(cols * rows)\n .fill(0)\n .map(() => glyphs[Math.floor(Math.random() * glyphs.length)])\n alphas = new Array(cols * rows)\n .fill(0)\n .map(() => 0.05 + Math.random() * 0.35)\n\n fgColor = readColor()\n }\n\n const draw = () => {\n const { clientWidth: w, clientHeight: h } = canvas\n ctx.clearRect(0, 0, w, h)\n\n const { r, g, b } = parseColor(fgColor)\n ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`\n ctx.textBaseline = \"top\"\n\n for (let y = 0; y < rows; y++) {\n const fade = fadeBottom > 0 ? 1 - (y / rows) * fadeBottom : 1\n for (let x = 0; x < cols; x++) {\n const i = y * cols + x\n const a = alphas[i] * fade\n ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`\n ctx.fillText(cells[i], x * cellSize, y * cellSize)\n }\n }\n }\n\n const tick = (t: number) => {\n if (stopped) return\n\n if (t - last >= interval) {\n last = t\n\n const total = cols * rows\n const mutations = Math.max(1, Math.floor(total * mutationRate))\n\n for (let n = 0; n < mutations; n++) {\n const i = Math.floor(Math.random() * total)\n cells[i] = glyphs[Math.floor(Math.random() * glyphs.length)]\n alphas[i] = 0.05 + Math.random() * 0.45\n }\n\n draw()\n }\n\n raf = requestAnimationFrame(tick)\n }\n\n resize()\n draw()\n raf = requestAnimationFrame(tick)\n\n const ro = new ResizeObserver(() => {\n resize()\n draw()\n })\n ro.observe(canvas)\n\n // Re-read color when theme changes (class on )\n const mo = new MutationObserver(() => {\n fgColor = readColor()\n draw()\n })\n mo.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\", \"data-theme\"],\n })\n\n return () => {\n stopped = true\n cancelAnimationFrame(raf)\n ro.disconnect()\n mo.disconnect()\n }\n }, [glyphs, cellSize, mutationRate, interval, fadeBottom])\n\n return (\n \n )\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/www/public/r/registry.json b/apps/www/public/r/registry.json
index 499944e16..b95bab771 100644
--- a/apps/www/public/r/registry.json
+++ b/apps/www/public/r/registry.json
@@ -442,6 +442,18 @@
}
]
},
+ {
+ "name": "glyph-matrix",
+ "type": "registry:ui",
+ "title": "Glyph Matrix",
+ "description": "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ "files": [
+ {
+ "path": "registry/magicui/glyph-matrix.tsx",
+ "type": "registry:ui"
+ }
+ ]
+ },
{
"name": "glare-hover",
"type": "registry:ui",
@@ -2039,6 +2051,21 @@
}
]
},
+ {
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "type": "registry:example"
+ }
+ ]
+ },
{
"name": "glare-hover-demo",
"type": "registry:example",
diff --git a/apps/www/public/registry.json b/apps/www/public/registry.json
index 499944e16..b95bab771 100644
--- a/apps/www/public/registry.json
+++ b/apps/www/public/registry.json
@@ -442,6 +442,18 @@
}
]
},
+ {
+ "name": "glyph-matrix",
+ "type": "registry:ui",
+ "title": "Glyph Matrix",
+ "description": "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ "files": [
+ {
+ "path": "registry/magicui/glyph-matrix.tsx",
+ "type": "registry:ui"
+ }
+ ]
+ },
{
"name": "glare-hover",
"type": "registry:ui",
@@ -2039,6 +2051,21 @@
}
]
},
+ {
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "type": "registry:example"
+ }
+ ]
+ },
{
"name": "glare-hover-demo",
"type": "registry:example",
diff --git a/apps/www/registry.json b/apps/www/registry.json
index 499944e16..b95bab771 100644
--- a/apps/www/registry.json
+++ b/apps/www/registry.json
@@ -442,6 +442,18 @@
}
]
},
+ {
+ "name": "glyph-matrix",
+ "type": "registry:ui",
+ "title": "Glyph Matrix",
+ "description": "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ "files": [
+ {
+ "path": "registry/magicui/glyph-matrix.tsx",
+ "type": "registry:ui"
+ }
+ ]
+ },
{
"name": "glare-hover",
"type": "registry:ui",
@@ -2039,6 +2051,21 @@
}
]
},
+ {
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "type": "registry:example"
+ }
+ ]
+ },
{
"name": "glare-hover-demo",
"type": "registry:example",
diff --git a/apps/www/registry/__index__.tsx b/apps/www/registry/__index__.tsx
index 8c1517ae8..35aa0bc8a 100644
--- a/apps/www/registry/__index__.tsx
+++ b/apps/www/registry/__index__.tsx
@@ -423,6 +423,23 @@ export const Index: Record = {
}),
meta: undefined,
},
+ "glyph-matrix": {
+ name: "glyph-matrix",
+ description: "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ type: "registry:ui",
+ registryDependencies: undefined,
+ files: [{
+ path: "registry/magicui/glyph-matrix.tsx",
+ type: "registry:ui",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/magicui/glyph-matrix.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name
+ return { default: mod.default ?? mod[exportName] }
+ }),
+ meta: undefined,
+ },
"glare-hover": {
name: "glare-hover",
description: "A diagonal glare on hover using a ::before gradient and CSS variables (angle, size, duration, color).",
@@ -2072,6 +2089,23 @@ export const Index: Record = {
}),
meta: undefined,
},
+ "glyph-matrix-demo": {
+ name: "glyph-matrix-demo",
+ description: "Example showing an animated grid of subtly shifting glyphs.",
+ type: "registry:example",
+ registryDependencies: ["@magicui/glyph-matrix"],
+ files: [{
+ path: "registry/example/glyph-matrix-demo.tsx",
+ type: "registry:example",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/example/glyph-matrix-demo.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name
+ return { default: mod.default ?? mod[exportName] }
+ }),
+ meta: undefined,
+ },
"glare-hover-demo": {
name: "glare-hover-demo",
description: "Pricing card with diagonal hover glare (duration 600ms).",
diff --git a/apps/www/registry/example/glyph-matrix-demo.tsx b/apps/www/registry/example/glyph-matrix-demo.tsx
new file mode 100644
index 000000000..0d17f4c1f
--- /dev/null
+++ b/apps/www/registry/example/glyph-matrix-demo.tsx
@@ -0,0 +1,15 @@
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+
+export default function GlyphMatrixDemo() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/www/registry/magicui/glyph-matrix.tsx b/apps/www/registry/magicui/glyph-matrix.tsx
new file mode 100644
index 000000000..ad2e32256
--- /dev/null
+++ b/apps/www/registry/magicui/glyph-matrix.tsx
@@ -0,0 +1,180 @@
+"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,
+ fadeBottom = 0.6,
+}: GlyphMatrixProps) {
+ 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: string[] = []
+ let alphas: number[] = []
+ let raf = 0
+ let last = 0
+ let stopped = false
+
+ const colorCanvas = document.createElement("canvas")
+ colorCanvas.width = 1
+ colorCanvas.height = 1
+ const colorContext = colorCanvas.getContext("2d")
+
+ const readColor = () => {
+ const probe = document.createElement("span")
+ probe.style.color = "var(--foreground)"
+ probe.style.display = "none"
+ document.body.appendChild(probe)
+ const computed = getComputedStyle(probe).color
+ probe.remove()
+ return computed
+ }
+
+ const parseColor = (value: string) => {
+ if (!colorContext) return { r: 0, g: 0, b: 0 }
+
+ colorContext.fillStyle = "#000"
+ colorContext.fillStyle = value
+ const normalized = colorContext.fillStyle
+ colorContext.fillStyle = normalized
+ colorContext.fillRect(0, 0, 1, 1)
+ const pixels = colorContext.getImageData(0, 0, 1, 1).data
+ const r = pixels[0]
+ const g = pixels[1]
+ const b = pixels[2]
+
+ return { r, g, b }
+ }
+
+ 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 draw = () => {
+ const { clientWidth: w, clientHeight: h } = canvas
+ ctx.clearRect(0, 0, w, h)
+
+ const { r, g, b } = parseColor(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 )
+ const mo = new MutationObserver(() => {
+ fgColor = readColor()
+ draw()
+ })
+ 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 (
+
+ )
+}
diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts
index a112db472..cfa8c5063 100644
--- a/apps/www/registry/registry-examples.ts
+++ b/apps/www/registry/registry-examples.ts
@@ -608,6 +608,19 @@ export const examples: Registry["items"] = [
},
],
},
+ {
+ name: "glyph-matrix-demo",
+ type: "registry:example",
+ title: "Glyph Matrix Demo",
+ description: "Example showing an animated grid of subtly shifting glyphs.",
+ registryDependencies: ["@magicui/glyph-matrix"],
+ files: [
+ {
+ path: "example/glyph-matrix-demo.tsx",
+ type: "registry:example",
+ },
+ ],
+ },
{
name: "glare-hover-demo",
type: "registry:example",
diff --git a/apps/www/registry/registry-ui.ts b/apps/www/registry/registry-ui.ts
index 0969e85f4..81f626874 100644
--- a/apps/www/registry/registry-ui.ts
+++ b/apps/www/registry/registry-ui.ts
@@ -412,6 +412,19 @@ export const ui: Registry["items"] = [
},
],
},
+ {
+ name: "glyph-matrix",
+ type: "registry:ui",
+ title: "Glyph Matrix",
+ description:
+ "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ files: [
+ {
+ path: "magicui/glyph-matrix.tsx",
+ type: "registry:ui",
+ },
+ ],
+ },
{
name: "glare-hover",
type: "registry:ui",
diff --git a/registry.json b/registry.json
index a9590e215..3c7fe0820 100644
--- a/registry.json
+++ b/registry.json
@@ -439,6 +439,19 @@
}
}
},
+ {
+ "name": "glyph-matrix",
+ "type": "registry:ui",
+ "title": "Glyph Matrix",
+ "description": "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ "files": [
+ {
+ "path": "registry/magicui/glyph-matrix.tsx",
+ "type": "registry:ui",
+ "target": "components/magicui/glyph-matrix.tsx"
+ }
+ ]
+ },
{
"name": "globe",
"type": "registry:ui",