Original by Plinth · MIT
Text Effects
Scramble Text
A word that dissolves into glyphs and resolves back on hover.
#text#scramble#hover#javascript
Install
npx shadcn@latest add https://plinthui.com/r/scramble-text.json'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
const GLYPHS = '!<>-_\\/[]{}—=+*^?#';
const STEP = 40; // ms between glyph cycles
const STAGGER = 3; // extra cycles each character waits before locking
export function ScrambleText({ text = 'DECRYPT' }: { text?: string }) {
const [display, setDisplay] = useState(text);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const mountedRef = useRef(true);
const stop = useCallback(() => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}, []);
// Clearing first means hovering mid-run restarts cleanly instead of
// leaving two intervals writing to the same span.
const run = useCallback(() => {
stop();
let frame = 0;
timerRef.current = setInterval(() => {
if (!mountedRef.current) {
stop();
return;
}
let out = '';
for (let i = 0; i < text.length; i += 1) {
out +=
frame >= (i + 1) * STAGGER
? text.charAt(i)
: GLYPHS.charAt(Math.floor(Math.random() * GLYPHS.length));
}
setDisplay(out);
if (frame >= text.length * STAGGER) stop();
frame += 1;
}, STEP);
}, [stop, text]);
useEffect(() => {
mountedRef.current = true;
run();
return () => {
mountedRef.current = false;
stop();
};
}, [run, stop]);
return (
<span
onMouseEnter={run}
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' }}
className="cursor-default text-[24px] tracking-[0.1em] text-[#1a1a1a] dark:text-[#ece9e2]"
>
{display}
</span>
);
}