"use client"
import { useEffect, useState } from "react"
import {
Matrix,
pulse,
snake,
wave,
type Frame,
} from "@/components/ui/matrix"
const Example = () => {
const [mode, setMode] = useState<
"individual" | "focus" | "expand" | "unified" | "collapse" | "burst"
>("individual")
const [unifiedFrame, setUnifiedFrame] = useState(0)
const [expandProgress, setExpandProgress] = useState(0)
const [collapseProgress, setCollapseProgress] = useState(0)
useEffect(() => {
let timeout: NodeJS.Timeout
if (mode === "individual") {
timeout = setTimeout(() => setMode("focus"), 4000)
} else if (mode === "focus") {
timeout = setTimeout(() => setMode("expand"), 2000)
} else if (mode === "expand") {
timeout = setTimeout(() => setMode("unified"), 2500)
} else if (mode === "unified") {
timeout = setTimeout(() => setMode("collapse"), 4000)
} else if (mode === "collapse") {
timeout = setTimeout(() => setMode("burst"), 2500)
} else if (mode === "burst") {
timeout = setTimeout(() => setMode("individual"), 800)
}
return () => clearTimeout(timeout)
}, [mode])
useEffect(() => {
if (mode !== "unified") return
let frame = 0
const animate = setInterval(() => {
frame += 1
setUnifiedFrame(frame)
}, 50)
return () => clearInterval(animate)
}, [mode])
useEffect(() => {
if (mode !== "expand") {
setExpandProgress(0)
return
}
let start = 0
const duration = 2500
let animationFrame: number
const animate = (timestamp: number) => {
if (start === 0) start = timestamp
const elapsed = timestamp - start
const progress = Math.min(elapsed / duration, 1)
setExpandProgress(progress)
if (progress < 1) {
animationFrame = requestAnimationFrame(animate)
}
}
animationFrame = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrame)
}, [mode])
useEffect(() => {
if (mode !== "collapse") {
setCollapseProgress(0)
return
}
let start = 0
const duration = 2500
let animationFrame: number
const animate = (timestamp: number) => {
if (start === 0) start = timestamp
const elapsed = timestamp - start
const progress = Math.min(elapsed / duration, 1)
setCollapseProgress(progress)
if (progress < 1) {
animationFrame = requestAnimationFrame(animate)
}
}
animationFrame = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrame)
}, [mode])
const configurations = [
{ animation: pulse, fps: 16 },
{ animation: wave, fps: 20 },
{ animation: spinner, fps: 10 },
{ animation: snake, fps: 15 },
{ animation: elevenLogo, fps: 12 },
{ animation: sandTimer, fps: 12 },
{ animation: corners, fps: 10 },
{ animation: sweep, fps: 14 },
{ animation: expand, fps: 12 },
]
const unifiedPatterns =
mode === "unified" ? createUnifiedPattern(unifiedFrame) : []
const expandedPatterns =
mode === "expand" ? createExpandedLogo(expandProgress) : []
const collapsePatterns =
mode === "collapse" ? createCollapseEffect(collapseProgress) : []
const getPatternForMatrix = (index: number) => {
if (mode === "individual") {
return undefined
} else if (mode === "focus") {
if (index === 4) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
for (let r = 1; r <= 5; r++) {
frame[r][2] = 1
frame[r][4] = 1
}
return frame
}
return Array(7)
.fill(0)
.map(() => Array(7).fill(0))
} else if (mode === "expand") {
return expandedPatterns[index]
} else if (mode === "unified") {
return unifiedPatterns[index]
} else if (mode === "collapse") {
return collapsePatterns[index]
} else if (mode === "burst") {
return undefined
}
}
const getFramesForMatrix = (index: number) => {
if (mode === "individual") {
return configurations[index].animation
} else if (mode === "burst") {
return burst
}
return undefined
}
const getFps = (index: number) => {
if (mode === "burst") return 30
return configurations[index].fps
}
return (
<div className="flex min-h-[600px] w-full flex-col items-center justify-center p-8">
<div
className="grid gap-1.5 transition-all duration-1000"
style={{
gridTemplateColumns: "repeat(3, 1fr)",
gridTemplateRows: "repeat(3, 1fr)",
}}
>
{configurations.map((config, index) => (
<div
key={index}
className="flex items-center justify-center transition-all duration-1000"
>
<Matrix
rows={7}
cols={7}
frames={getFramesForMatrix(index)}
pattern={getPatternForMatrix(index)}
fps={getFps(index)}
size={10}
gap={2}
ariaLabel={`Matrix ${index + 1}`}
/>
</div>
))}
</div>
</div>
)
}
function createUnifiedPattern(frameIndex: number): Frame[] {
const totalRows = 21
const totalCols = 21
const pattern: number[][] = []
for (let row = 0; row < totalRows; row++) {
pattern[row] = []
for (let col = 0; col < totalCols; col++) {
const centerX = totalCols / 2
const centerY = totalRows / 2
const distance = Math.sqrt(
Math.pow(col - centerX, 2) + Math.pow(row - centerY, 2)
)
const wave = Math.sin(distance * 0.5 - frameIndex * 0.2)
const value = (wave + 1) / 2
pattern[row][col] = value
}
}
const matrices: Frame[] = []
for (let matrixRow = 0; matrixRow < 3; matrixRow++) {
for (let matrixCol = 0; matrixCol < 3; matrixCol++) {
const matrixFrame: Frame = []
for (let row = 0; row < 7; row++) {
matrixFrame[row] = []
for (let col = 0; col < 7; col++) {
const globalRow = matrixRow * 7 + row
const globalCol = matrixCol * 7 + col
matrixFrame[row][col] = pattern[globalRow][globalCol]
}
}
matrices.push(matrixFrame)
}
}
return matrices
}
const elevenLogo: Frame[] = (() => {
const frames: Frame[] = []
const totalFrames = 30
for (let f = 0; f < totalFrames; f++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
const phase = (f / totalFrames) * Math.PI * 2
const intensity = ((Math.sin(phase) + 1) / 2) * 0.3 + 0.7
for (let r = 1; r <= 5; r++) {
frame[r][2] = intensity
frame[r][4] = intensity
}
frames.push(frame)
}
return frames
})()
const sandTimer: Frame[] = (() => {
const frames: Frame[] = []
const totalFrames = 60
for (let f = 0; f < totalFrames; f++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
frame[0][2] = 1
frame[0][3] = 1
frame[0][4] = 1
frame[1][2] = 1
frame[1][4] = 1
frame[5][2] = 1
frame[5][4] = 1
frame[6][2] = 1
frame[6][3] = 1
frame[6][4] = 1
const progress = f / totalFrames
const topSand = Math.floor((1 - progress) * 8)
for (let i = 0; i < topSand; i++) {
if (i < 3) frame[1][3] = 1
if (i >= 3) frame[2][3] = 1
}
const bottomSand = Math.floor(progress * 8)
for (let i = 0; i < bottomSand; i++) {
if (i < 3) frame[5][3] = 1
if (i >= 3 && i < 6) frame[4][3] = 1
if (i >= 6) frame[3][3] = 0.5
}
frames.push(frame)
}
return frames
})()
const spinner: Frame[] = (() => {
const frames: Frame[] = []
const segments = 8
for (let f = 0; f < segments; f++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
const positions = [
[1, 3],
[1, 4],
[2, 5],
[3, 5],
[4, 5],
[5, 4],
[5, 3],
[5, 2],
[4, 1],
[3, 1],
[2, 1],
[1, 2],
]
for (let i = 0; i < 3; i++) {
const idx = (f + i) % positions.length
const [r, c] = positions[idx]
frame[r][c] = 1 - i * 0.3
}
frames.push(frame)
}
return frames
})()
const corners: Frame[] = (() => {
const frames: Frame[] = []
for (let i = 0; i < 16; i++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
const progress = i / 16
for (let r = 0; r < 7; r++) {
for (let c = 0; c < 7; c++) {
const distFromCorner = Math.min(
Math.sqrt(r * r + c * c),
Math.sqrt(r * r + (6 - c) * (6 - c)),
Math.sqrt((6 - r) * (6 - r) + c * c),
Math.sqrt((6 - r) * (6 - r) + (6 - c) * (6 - c))
)
const threshold = progress * 8
if (distFromCorner <= threshold) {
frame[r][c] = Math.max(0, 1 - Math.abs(distFromCorner - threshold))
}
}
}
frames.push(frame)
}
return frames
})()
const sweep: Frame[] = (() => {
const frames: Frame[] = []
for (let i = 0; i < 14; i++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
for (let r = 0; r < 7; r++) {
for (let c = 0; c < 7; c++) {
if (r + c === i) {
frame[r][c] = 1
} else if (r + c === i - 1) {
frame[r][c] = 0.5
} else if (r + c === i + 1) {
frame[r][c] = 0.5
}
}
}
frames.push(frame)
}
return frames
})()
const expand: Frame[] = (() => {
const frames: Frame[] = []
for (let i = 0; i <= 6; i++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
for (let r = 3 - i; r <= 3 + i; r++) {
for (let c = 3 - i; c <= 3 + i; c++) {
if (r >= 0 && r < 7 && c >= 0 && c < 7) {
if (r === 3 - i || r === 3 + i || c === 3 - i || c === 3 + i) {
frame[r][c] = 1
}
}
}
}
frames.push(frame)
}
for (let i = 5; i >= 0; i--) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
for (let r = 3 - i; r <= 3 + i; r++) {
for (let c = 3 - i; c <= 3 + i; c++) {
if (r >= 0 && r < 7 && c >= 0 && c < 7) {
if (r === 3 - i || r === 3 + i || c === 3 - i || c === 3 + i) {
frame[r][c] = 1
}
}
}
}
frames.push(frame)
}
return frames
})()
const burst: Frame[] = (() => {
const frames: Frame[] = []
for (let f = 0; f < 8; f++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
const intensity = f < 4 ? f / 3 : (7 - f) / 3
if (f < 6) {
for (let r = 0; r < 7; r++) {
for (let c = 0; c < 7; c++) {
const distance = Math.sqrt(Math.pow(r - 3, 2) + Math.pow(c - 3, 2))
if (Math.abs(distance - f * 0.8) < 1.2) {
frame[r][c] = intensity
}
}
}
}
frames.push(frame)
}
return frames
})()
function createExpandedLogo(progress: number): Frame[] {
const matrices: Frame[] = []
for (let matrixIdx = 0; matrixIdx < 9; matrixIdx++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
matrices.push(frame)
}
const easeProgress =
progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2
if (easeProgress < 0.3) {
const centerMatrix = matrices[4]
for (let r = 1; r <= 5; r++) {
centerMatrix[r][2] = 1
centerMatrix[r][4] = 1
}
return matrices
}
const expandProgress = (easeProgress - 0.3) / 0.7
for (let globalRow = 0; globalRow < 21; globalRow++) {
for (let globalCol = 0; globalCol < 21; globalCol++) {
const matrixRow = Math.floor(globalRow / 7)
const matrixCol = Math.floor(globalCol / 7)
const matrixIdx = matrixRow * 3 + matrixCol
const localRow = globalRow % 7
const localCol = globalCol % 7
const leftBarStart = Math.floor(9 + (5 - 9) * expandProgress)
const leftBarEnd = Math.floor(9 + (7 - 9) * expandProgress)
const rightBarStart = Math.floor(11 + (13 - 11) * expandProgress)
const rightBarEnd = Math.floor(11 + (15 - 11) * expandProgress)
const isLeftBar = globalCol >= leftBarStart && globalCol <= leftBarEnd
const isRightBar = globalCol >= rightBarStart && globalCol <= rightBarEnd
const inVerticalRange = globalRow >= 4 && globalRow <= 16
if ((isLeftBar || isRightBar) && inVerticalRange) {
matrices[matrixIdx][localRow][localCol] = 1
}
}
}
return matrices
}
function createCollapseEffect(progress: number): Frame[] {
const matrices: Frame[] = []
for (let matrixIdx = 0; matrixIdx < 9; matrixIdx++) {
const frame: Frame = Array(7)
.fill(0)
.map(() => Array(7).fill(0))
matrices.push(frame)
}
const easeProgress =
progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2
if (easeProgress < 0.4) {
const collapseProgress = easeProgress / 0.4
for (let globalRow = 0; globalRow < 21; globalRow++) {
for (let globalCol = 0; globalCol < 21; globalCol++) {
const matrixRow = Math.floor(globalRow / 7)
const matrixCol = Math.floor(globalCol / 7)
const matrixIdx = matrixRow * 3 + matrixCol
const localRow = globalRow % 7
const localCol = globalCol % 7
const leftBarStart = Math.floor(5 + (9 - 5) * collapseProgress)
const leftBarEnd = Math.floor(7 + (9 - 7) * collapseProgress)
const rightBarStart = Math.floor(13 + (11 - 13) * collapseProgress)
const rightBarEnd = Math.floor(15 + (11 - 15) * collapseProgress)
const isLeftBar = globalCol >= leftBarStart && globalCol <= leftBarEnd
const isRightBar =
globalCol >= rightBarStart && globalCol <= rightBarEnd
const inVerticalRange = globalRow >= 4 && globalRow <= 16
if ((isLeftBar || isRightBar) && inVerticalRange) {
matrices[matrixIdx][localRow][localCol] = 1
}
}
}
} else {
const centerMatrix = matrices[4]
const fadeProgress = (easeProgress - 0.4) / 0.6
const brightness = 1 - fadeProgress
for (let r = 1; r <= 5; r++) {
centerMatrix[r][2] = brightness
centerMatrix[r][4] = brightness
}
}
return matrices
}
export Example
Installation
pnpm dlx @elevenlabs/agents-cli@latest components add matrix
Usage
import { digits, loader, Matrix, vu, wave } from "@/components/ui/matrix"
Static Pattern
<Matrix rows={7} cols={5} pattern={digits[5]} ariaLabel="Number five" />
Animated Display
<Matrix
rows={7}
cols={7}
frames={wave}
fps={20}
loop
ariaLabel="Wave animation"
/>
VU Meter
<Matrix
rows={7}
cols={12}
mode="vu"
levels={[0.1, 0.6, 0.9, 0.7, 0.4, 0.8, 0.5, 0.3, 0.6, 0.9, 0.5, 0.2]}
/>
API Reference
Matrix
The main matrix display component.
<Matrix rows={7} cols={7} frames={wave} fps={20} />
Props
Prop | Type | Description |
---|---|---|
rows | number | Number of rows in the matrix (required) |
cols | number | Number of columns in the matrix (required) |
pattern | Frame | Static pattern to display (2D array of brightness values 0-1) |
frames | Frame[] | Array of frames for animation (ignored if pattern is provided) |
fps | number | Frames per second for animation. Default: 12 |
autoplay | boolean | Start animation automatically. Default: true |
loop | boolean | Loop animation. Default: true |
size | number | Cell size in pixels. Default: 10 |
gap | number | Gap between cells in pixels. Default: 2 |
palette | {on: string, off: string} | CSS colors for on/off states. Defaults to theme colors |
brightness | number | Global brightness multiplier (0-1). Default: 1 |
ariaLabel | string | ARIA label for accessibility |
onFrame | (index: number) => void | Callback when frame changes during animation |
mode | "default" | "vu" | Display mode. Default: "default" |
levels | number[] | Live levels for VU meter mode (0-1 per column) |
className | string | Additional CSS classes |
...props | React.HTMLAttributes<HTMLDivElement> | All standard div element props |
Frame Type
type Frame = number[][] // [row][col] brightness 0..1
A frame is a 2D array where each value represents the brightness of a cell (0 = off, 1 = full brightness).
Example:
const smiley: Frame = [
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[1, 1, 0, 1, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
]
Presets
The component comes with several built-in presets and animations:
digits
7-segment style digits (0-9) on a 7×5 grid.
import { digits, Matrix } from "@/components/ui/matrix"
export default () => <Matrix rows={7} cols={5} pattern={digits[5]} />
loader
Rotating spinner animation (7×7, 12 frames).
import { loader, Matrix } from "@/components/ui/matrix"
export default () => <Matrix rows={7} cols={7} frames={loader} fps={12} />
pulse
Expanding pulse effect (7×7, 16 frames).
import { Matrix, pulse } from "@/components/ui/matrix"
export default () => <Matrix rows={7} cols={7} frames={pulse} fps={16} />
wave
Smooth sine wave animation (7×7, 24 frames).
import { Matrix, wave } from "@/components/ui/matrix"
export default () => <Matrix rows={7} cols={7} frames={wave} fps={20} />
snake
Snake traversal pattern (7×7, ~40 frames).
import { Matrix, snake } from "@/components/ui/matrix"
export default () => <Matrix rows={7} cols={7} frames={snake} fps={15} />
chevronLeft / chevronRight
Simple directional arrows (5×5).
import { chevronLeft, Matrix } from "@/components/ui/matrix"
export default () => <Matrix rows={5} cols={5} pattern={chevronLeft} />
vu()
Helper function to create VU meter frames.
import { Matrix, vu } from "@/components/ui/matrix"
export default () => {
const levels = [0.3, 0.6, 0.9, 0.7, 0.5]
return <Matrix rows={7} cols={5} pattern={vu(5, levels)} />
}
Examples
Retro Display
function RetroDisplay() {
return (
<div className="bg-muted/30 rounded-lg border p-8">
<Matrix
rows={7}
cols={7}
frames={wave}
fps={20}
size={16}
gap={3}
palette={{
on: "hsl(142 76% 36%)",
off: "hsl(142 76% 10%)",
}}
ariaLabel="Wave animation"
/>
</div>
)
}
Digital Clock Digit
function ClockDigit({ value }: { value: number }) {
return (
<Matrix
rows={7}
cols={5}
pattern={digits[value]}
size={12}
gap={2}
ariaLabel={`Digit ${value}`}
/>
)
}
Audio Level Meter
function AudioMeter({ frequencyData }: { frequencyData: number[] }) {
// Convert frequency data to 0-1 levels
const levels = frequencyData.map((freq) => freq / 255)
return (
<Matrix
rows={7}
cols={frequencyData.length}
mode="vu"
levels={levels}
size={8}
gap={1}
ariaLabel="Audio frequency meter"
/>
)
}
Custom Pattern
function Heart() {
const heartPattern: Frame = [
[0, 1, 1, 0, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
]
return (
<Matrix
rows={6}
cols={7}
pattern={heartPattern}
size={14}
gap={2}
palette={{
on: "hsl(0 84% 60%)",
off: "hsl(0 84% 20%)",
}}
/>
)
}
Advanced Usage
Creating Custom Animations
function CustomAnimation() {
// Create a simple blink animation
const frames: Frame[] = [
[
[1, 0, 1],
[0, 1, 0],
[1, 0, 1],
], // Frame 1
[
[0, 1, 0],
[1, 0, 1],
[0, 1, 0],
], // Frame 2
]
return <Matrix rows={3} cols={3} frames={frames} fps={2} loop />
}
Frame Change Callback
function AnimationTracker() {
const [currentFrame, setCurrentFrame] = useState(0)
return (
<div>
<Matrix
rows={7}
cols={7}
frames={loader}
fps={12}
onFrame={setCurrentFrame}
/>
<p>Frame: {currentFrame}</p>
</div>
)
}
Dynamic VU Meter
function LiveVUMeter() {
const [levels, setLevels] = useState(Array(12).fill(0))
useEffect(() => {
// Simulate audio levels
const interval = setInterval(() => {
setLevels(Array.from({ length: 12 }, () => Math.random()))
}, 50)
return () => clearInterval(interval)
}, [])
return (
<Matrix rows={7} cols={12} mode="vu" levels={levels} size={10} gap={2} />
)
}
Theming
The component uses CSS variables for theming:
<Matrix
palette={{
on: "currentColor", // Active cells (default - inherits text color)
off: "var(--muted-foreground)", // Inactive cells (default - muted but visible)
}}
/>
Classic Phosphor Green
palette={{
on: "hsl(142 76% 36%)",
off: "hsl(142 76% 10%)",
}}
Amber Terminal
palette={{
on: "hsl(38 92% 50%)",
off: "hsl(38 92% 15%)",
}}
Blue Neon
palette={{
on: "hsl(200 98% 39%)",
off: "hsl(200 98% 12%)",
}}
Performance
- Uses SVG for crisp rendering at any size
- Cell positions are precomputed and memoized
- Only opacity updates during frame transitions
- Stable FPS with time accumulator and
requestAnimationFrame
- Tested with 7×7 (49 cells) and 16×16 (256 cells) grids
- Proper cleanup of animation frames on unmount
Accessibility
- Container has
role="img"
for semantic meaning - Configurable
aria-label
for description - Animated displays use
aria-live="polite"
- Frame information available via
onFrame
callback - All interactive demos support keyboard navigation
Notes
- Frames use brightness values from 0 (off) to 1 (full on)
- Circular cells provide a classic dot-matrix appearance
- VU meter mode provides real-time column-based visualization
- All presets are optimized for 7×7 or similar small grids
- Works in SSR environments (animation starts on mount)
- Compatible with all modern browsers
- Supports both light and dark themes
On This Page
InstallationUsageStatic PatternAnimated DisplayVU MeterAPI ReferenceMatrixPropsFrame TypePresetsdigitsloaderpulsewavesnakechevronLeft / chevronRightvu()ExamplesRetro DisplayDigital Clock DigitAudio Level MeterCustom PatternAdvanced UsageCreating Custom AnimationsFrame Change CallbackDynamic VU MeterThemingClassic Phosphor GreenAmber TerminalBlue NeonPerformanceAccessibilityNotesDeploy and Scale Agents with ElevenLabs
ElevenLabs delivers the infrastructure and developer experience you need to ship reliable audio & agent applications at scale.
Talk to an expert