Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.
Full-width hero section with cascading matrix rain and centered green-phosphor glow text.
MSource LandMellow UIWhy it stands out
Full-width hero section with cascading matrix rain and centered green-phosphor glow text.
You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11
If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.
Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.
Copy-paste these files:
File location: components/matrix-hero-demo.tsx
File content: "use client";
import MatrixHero from "@/registry/mellow/matrix-hero";
export default function MatrixHeroDemo() {
return (
<div style={{ width: "100%", height: "100%" }}>
<MatrixHero />
</div>
);
}
File location: components/mellow/matrix-hero.tsx
File content: "use client";
import React, { useEffect, useRef, useState } from "react";
const DEFAULT_CHARSET =
"ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン01234567";
const TRAIL = 10;
const FONT_SIZE = 14;
const CHAR_DELAY = 60;
const LINE_PAUSE = 150;
const BLINK_MS = 530;
function parseLines(text: string | string[]): string[] {
if (Array.isArray(text)) return text.filter(Boolean);
return text.split("\n").filter(Boolean);
}
export interface MatrixHeroProps {
text?: string | string[];
speed?: number;
paused?: boolean;
charset?: string;
glowIntensity?: number;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
export function MatrixHero({
text = ["Welcome to the Matrix,", "Neo."],
speed = 1,
paused = false,
charset = DEFAULT_CHARSET,
glowIntensity = 1,
className,
style,
children,
}: MatrixHeroProps) {
useEffect(() => {
if (document.getElementById("vt323-font")) return;
const link = document.createElement("link");
link.id = "vt323-font";
link.rel = "stylesheet";
link.href = "https://fonts.googleapis.com/css2?family=VT323&display=swap";
document.head.appendChild(link);
}, []);
const canvasRef = useRef<HTMLCanvasElement>(null);
const sizeRef = useRef({ w: 0, h: 0 });
const rainRef = useRef<{ head: number; spd: number }[]>([]);
const frameRef = useRef(0);
const pausedRef = useRef(paused);
useEffect(() => { pausedRef.current = paused; }, [paused]);
useEffect(() => {
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!canvasRef.current) return;
const canvas: HTMLCanvasElement = canvasRef.current;
const ctxOrNull = canvas.getContext("2d");
if (!ctxOrNull) return;
const ctx: CanvasRenderingContext2D = ctxOrNull;
function resize() {
const dpr = Math.min(window.devicePixelRatio, 2);
const rect = canvas.getBoundingClientRect();
sizeRef.current = { w: rect.width, h: rect.height };
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
}
const ro = new ResizeObserver(resize);
ro.observe(canvas);
resize();
function draw() {
const { w, h } = sizeRef.current;
if (w === 0 || h === 0) { frameRef.current = requestAnimationFrame(draw); return; }
ctx.font = `${FONT_SIZE}px monospace`;
const charW = ctx.measureText("M").width;
const charH = FONT_SIZE * 1.2;
const cols = Math.floor(w / charW);
const rows = Math.floor(h / charH);
if (rainRef.current.length !== cols) {
rainRef.current = Array.from({ length: cols }, (_, ci) => ({
head: (Math.sin(ci * 7.3) * 0.5 + 0.5) * rows,
spd: 0.08 + ((ci * 13) % 17) / 17 * 0.25,
}));
}
if (!reduced && !pausedRef.current) {
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, w, h);
ctx.font = `${FONT_SIZE}px monospace`;
ctx.textBaseline = "top";
for (let ci = 0; ci < cols; ci++) {
const r = rainRef.current[ci];
if (!r) continue;
r.head += r.spd * speed;
if (r.head - TRAIL > rows) {
r.head = -(Math.random() * rows * 0.3);
r.spd = 0.08 + Math.random() * 0.25;
}
}
for (let ci = 0; ci < cols; ci++) {
const rain = rainRef.current[ci];
if (!rain) continue;
for (let ri = 0; ri < rows; ri++) {
const dist = rain.head - ri;
if (dist < 0 || dist > TRAIL) continue;
const t = 1 - dist / TRAIL;
const isHead = dist < 1.2;
const chIdx =
Math.abs(ci * 31 + ri * 17 + Math.floor(rain.head * 2.5)) %
charset.length;
const ch = charset[chIdx] ?? "0";
ctx.fillStyle = isHead
? `rgba(190,255,190,${(t * 0.85).toFixed(2)})`
: `rgba(0,${Math.floor(55 + t * 130)},0,${(t * 0.5).toFixed(2)})`;
ctx.fillText(ch, ci * charW, ri * charH);
}
}
}
frameRef.current = requestAnimationFrame(draw);
}
frameRef.current = requestAnimationFrame(draw);
return () => { cancelAnimationFrame(frameRef.current); ro.disconnect(); };
}, [speed, charset]);
const lines = parseLines(Array.isArray(text) ? text : (text ?? ""));
const [displayed, setDisplayed] = useState<string[]>(() => lines.map(() => ""));
const [activeLine, setActiveLine] = useState(0);
const [typingDone, setTypingDone] = useState(false);
const [cursorOn, setCursorOn] = useState(true);
const textKey = typeof text === "string" ? text : JSON.stringify(text);
useEffect(() => {
const ls = parseLines(Array.isArray(text) ? text : (text ?? ""));
setDisplayed(ls.map(() => ""));
setActiveLine(0);
setTypingDone(false);
setCursorOn(true);
let lineIdx = 0;
let charIdx = 0;
let timer: ReturnType<typeof setTimeout>;
function typeNext() {
if (pausedRef.current) { timer = setTimeout(typeNext, 50); return; }
const target = ls[lineIdx];
if (!target) { setTypingDone(true); return; }
if (charIdx < target.length) {
charIdx++;
const snap = charIdx;
const snap_l = lineIdx;
setDisplayed(prev => {
const next = [...prev];
next[snap_l] = target.slice(0, snap);
return next;
});
timer = setTimeout(typeNext, CHAR_DELAY);
} else if (lineIdx < ls.length - 1) {
lineIdx++;
charIdx = 0;
setActiveLine(lineIdx);
timer = setTimeout(typeNext, LINE_PAUSE);
} else {
setTypingDone(true);
}
}
timer = setTimeout(typeNext, CHAR_DELAY);
return () => clearTimeout(timer);
}, [textKey]);
useEffect(() => {
const id = setInterval(() => setCursorOn(v => !v), BLINK_MS);
return () => clearInterval(id);
}, []);
const g = glowIntensity;
const textShadow =
g > 0
? `0 0 ${7 * g}px #00FF41, 0 0 ${16 * g}px #00FF41, 0 0 ${32 * g}px rgba(0,255,65,0.45)`
: "none";
return (
<div
className={className}
style={{
position: "relative",
width: "100%",
height: "100%",
background: "#000",
overflow: "hidden",
...style,
}}
>
<canvas
ref={canvasRef}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }}
/>
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
padding: "2rem",
pointerEvents: "none",
}}
>
{children ?? (
<div style={{ textAlign: "center" }}>
{lines.map((_, i) => {
const showCursor = i === activeLine && !typingDone
? true
: i === lines.length - 1 && typingDone;
return (
<div
key={i}
style={{
fontFamily: "'VT323', monospace",
fontSize: "clamp(1.1rem, 2.8vw, 2.25rem)",
fontWeight: 700,
color: "#00FF41",
textShadow,
lineHeight: 1.4,
letterSpacing: "0.04em",
whiteSpace: "pre",
minHeight: "1.4em",
}}
>
{displayed[i] ?? ""}
{showCursor && (
<span style={{
opacity: (i === activeLine && !typingDone) ? 1 : (cursorOn ? 1 : 0),
display: "inline-block",
transform: "scaleY(1.5) translateY(-0.1em)",
transformOrigin: "center",
}}>
▮
</span>
)}
</div>
);
})}
</div>
)}
</div>
</div>
);
}
export default MatrixHero;
Usage notes:
Add a MatrixHero component from the mellow library. It renders a full-width hero section with cascading half-width katakana matrix rain as the background and centered green-phosphor glow text on top. Pass text='...' for a single headline, or children for custom content. Key props: text, speed, paused, charset, glowIntensity.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI