Backgrounds
Digital rain with flickering katakana and fading trails.
$ ssh neo@zion
wake up, neo…Controls
pnpm dlx shadcn@latest add @asciicn/ascii-matrixPulls in the engine at lib/ascii on first install. The @asciicn namespace needs one line in components.json, see Installation.
Install the engine first with npx shadcn@latest add @asciicn/ascii-core, or copy lib/ascii from the repository.
Copy this into components/ascii/ascii-matrix.tsx:
"use client"
import { useRef } from "react"
import { AsciiCanvas, charsets, type AsciiCanvasProps, type AsciiFrame } from "@/lib/ascii"
import { cn } from "@/lib/utils"
export type AsciiMatrixProps = Omit<AsciiCanvasProps, "field" | "frame" | "source"> & {
/** Characters the rain is made of. */
glyphs?: string
/** Rows per second for an average drop. */
speed?: number
/** 0 to 1, share of columns with a falling drop. */
density?: number
}
type Drop = { y: number; speed: number; length: number }
export function AsciiMatrix({
glyphs = charsets.katakana.trim() + "0123456789",
speed = 14,
density = 0.7,
className,
...props
}: AsciiMatrixProps) {
const drops = useRef<Drop[]>([])
const last = useRef(0)
const codes = Array.from(glyphs, (c) => c.charCodeAt(0))
// Lower density starts drops further above the top edge, which leaves longer gaps between them.
const spawn = (rows: number, onScreen: boolean): Drop => ({
y: onScreen ? Math.random() * rows : (-Math.random() * rows) / Math.max(0.05, density),
speed: speed * (0.5 + Math.random()),
length: 6 + Math.random() * rows * 0.6,
})
const frame: AsciiFrame = (f, t) => {
const dt = Math.min(0.1, Math.max(0, t - last.current))
last.current = t
if (drops.current.length !== f.cols) {
drops.current = Array.from({ length: f.cols }, () => spawn(f.rows, true))
}
for (let c = 0; c < f.cols; c++) {
const d = drops.current[c]
d.y += d.speed * dt
if (d.y - d.length > f.rows) drops.current[c] = spawn(f.rows, false)
const head = Math.floor(d.y)
for (let k = 0; k < d.length; k++) {
const r = head - k
if (r < 0 || r >= f.rows) continue
const i = r * f.cols + c
f.tone[i] = k === 0 ? 1 : 0.9 * (1 - k / d.length)
// Glyphs flicker at their own pace instead of scrolling with the drop.
const seed = (c * 7919 + r * 104729 + Math.floor(t * (2 + (c % 5)))) % codes.length
f.glyph[i] = codes[seed]
}
}
}
return (
<AsciiCanvas
aria-hidden
role="presentation"
frame={frame}
opacity
className={cn("pointer-events-none", className)}
{...props}
/>
)
}
import { AsciiMatrix } from "@/components/ascii/ascii-matrix"
<div className="h-96">
<AsciiMatrix className="text-primary" />
</div>Any string works as glyphs. Pair it with a palette for a green phosphor look that ignores the theme colour.
| Prop | Type | Default | Description |
|---|---|---|---|
| glyphs | string | katakana + digits | Characters the rain is made of. |
| speed | number | 14 | Rows per second for an average drop. |
| density | number | 0.7 | 0 to 1. Lower leaves longer gaps. |
| Prop | Type | Default | Description |
|---|---|---|---|
| charset | CharsetName | string | "standard" | Named ramp or your own string, empty to dense. "braille" packs 2×4 dots per cell, "quadrants" 2×2 blocks. |
| fontSize | number | 12 | Cell size in CSS pixels. Smaller means more detail. |
| dither | "none" | "bayer" | "floyd" | "none" | Quantisation. Floyd–Steinberg for photos, Bayer for a retro look. |
| color | "ink" | "source" | string[] | "ink" | Ink uses the CSS colour (theme with text-*). Source keeps pixel colours. An array maps tone onto gradient stops, e.g. palettes.thermal. |
| edges | number | 0 | Outline contours with | / - \ where the tone gradient beats this strength. Try 0.3. Ramps only. |
| opacity | boolean | false | Fade each glyph by its tone. |
| brightness | number | 0 | Added after contrast, -1 to 1. |
| contrast | number | 1 | Multiplier around mid grey. |
| gamma | number | 1 | Tone curve exponent. |
| invert | boolean | false | Swap ink and blank. |
| animate | boolean | auto | On for fields, frames and video. Off under reduced motion, except video. |
| fps | number | 30 | Frame cap. |
| ref | Ref<AsciiHandle> | — | Exposes text(), svg(), png() and the canvas. |
aria-hidden. Put real content on top of it.prefers-reduced-motion it freezes on the current frame.color, so text-primary or text-foreground/40 themes them. Theme changes repaint at once.ref.current.text(), svg() or png().