← Back
Animation

Text Scramble

Characters randomize through a charset before revealing the final text — triggered on hover.

Hello, World.

#classic — hover to scramble

Frontend Magic.

#creative — hover to scramble

CSS is art.

#opinion — hover to scramble

Ship fast.

#mantra — hover to scramble

TextScramble.tsx69 lines
"use client";

import { useEffect, useState } from "react";

const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*?";

function ScrambleText({ text }: { text: string }) {
  const [display, setDisplay] = useState(text);

  useEffect(() => {
    let progress = 0;
    const id = setInterval(() => {
      setDisplay(
        text
          .split("")
          .map((char, i) => {
            if (char === " ") return " ";
            if (i < Math.floor(progress)) return char;
            return CHARSET[Math.floor(Math.random() * CHARSET.length)];
          })
          .join(""),
      );
      progress += 0.35;
      if (progress > text.length) {
        clearInterval(id);
        setDisplay(text);
      }
    }, 28);
    return () => clearInterval(id);
  }, [text]);

  return <>{display}</>;
}

const phrases = [
  { text: "Hello, World.", hint: "classic" },
  { text: "Frontend Magic.", hint: "creative" },
  { text: "CSS is art.", hint: "opinion" },
  { text: "Ship fast.", hint: "mantra" },
];

function ScrambleBlock({ text, hint }: { text: string; hint: string }) {
  const [key, setKey] = useState(0);

  return (
    <div
      onMouseEnter={() => setKey((k) => k + 1)}
      className="group px-6 py-6 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 cursor-default hover:border-zinc-400 dark:hover:border-zinc-600 transition-colors duration-200"
    >
      <p className="font-mono text-xl font-bold text-zinc-900 dark:text-zinc-50 tracking-tight min-h-[1.75rem]">
        {key === 0 ? text : <ScrambleText key={key} text={text} />}
      </p>
      <p className="text-xs text-zinc-400 dark:text-zinc-600 mt-2 font-mono">
        #{hint} — hover to scramble
      </p>
    </div>
  );
}

export default function TextScramble() {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
      {phrases.map(({ text, hint }) => (
        <ScrambleBlock key={text} text={text} hint={hint} />
      ))}
    </div>
  );
}

More in Animation