Building a Scramble-on-Hover Text Effect
A tutorial on crafting the cyberpunk-style text scramble effect with pure React, timers, and proper cleanup — no external libs required.
In this tutorial, we'll build the Scramble Hover effect step by step. Hover the preview text after each step to see what you've unlocked.
Base Text
Start with a simple button rendering a string.
<button>{text}</button>Scramble Function
Write a pure function that replaces every character (except spaces) with a random one.
const CHARACTERS = "ABC...xyz0123!@#".split("");function scrambleText(original: string) { return original .split("") .map((c) => c === " " ? " " : CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)] ) .join("");}Interval Loop
On hover, run setInterval to reshuffle the text every few milliseconds.
const [display, setDisplay] = useState(text);const handleMouseEnter = () => { intervalRef.current = setInterval(() => { setDisplay(scrambleText(text)); }, 30);};Stop & Restore
Use setTimeout so the scramble lasts a fixed duration, then reset to the original text.
const handleMouseEnter = () => { intervalRef.current = setInterval(() => { setDisplay(scrambleText(text)); }, 30); timeoutRef.current = setTimeout(() => { clearInterval(intervalRef.current!); setDisplay(text); // snap back to original }, 600);};Cleanup on Leave
Clear timers when the pointer leaves so animations don't leak or overlap.
const handleMouseLeave = () => { clearInterval(intervalRef.current!); clearTimeout(timeoutRef.current!); setDisplay(text);};Accessibility
Detect hover-capable devices and respect prefers-reduced-motion.
useEffect(() => { const motion = matchMedia("(prefers-reduced-motion: reduce)"); const hover = matchMedia("(hover: hover) and (pointer: fine)"); setReduced(motion.matches); setHasHover(hover.matches);}, []);const onEnter = reduced || !hasHover ? undefined : handleMouseEnter;Key Takeaways
After building this component, you've learned:
- Pure, stateless scramble functions are easy to test and reuse
- Preserve spaces so the silhouette of the text stays readable
- 30ms intervals feel glitchy without being unreadable; 600ms total is a natural duration
- Refs for timers — never put
setIntervalIDs in state - Always clean up both timers on leave; otherwise hovers can stack
- Gate on
hover:hoverso touch devices don't fire the effect on tap
Install the Component
npx smoothui-cli add scramble-hoverCheck out the full documentation for all props and variations.