Skip to main content
Back to discovery

Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.

Matrix Hero

Full-width hero section with cascading matrix rain and centered green-phosphor glow text.

MSource LandMellow UI

Why it stands out

Full-width hero section with cascading matrix rain and centered green-phosphor glow text.

Prompt

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

Continue comparing

More from Mellow UI