Jumbled Text Hover Effect
A playful text-scrambling effect that jumbles your text on hover (or on mount), then gradually reveals the original string.
Modern UI Component Library
KINESISis motion triggered by a stimulus.
Create stunning, interactive experiences that captivate your users.
Install required dependencies
npm install clsx tailwind-merge
Copy the following source code
"use client";
import { cn } from '@/lib/utils';
import React, { useState, useRef, useEffect } from 'react';
interface JumbledTextProps {
text: string;
className?: string;
jumbleDuration?: number;
revealDuration?: number;
jumbleInterval?: number;
animateOnMount?: boolean;
mountDelay?: number;
}
const shuffleArray = <T,>(array: T[]): T[] => {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
};
const scrambleCharacters = (str: string): string => {
return shuffleArray([...str]).join('');
};
const JumbledText: React.FC<JumbledTextProps> = ({
text,
className,
jumbleDuration = 300,
revealDuration = 40,
jumbleInterval = 50,
animateOnMount = false,
mountDelay = 10,
}) => {
const [display, setDisplay] = useState(text);
const [hasMounted, setHasMounted] = useState(false);
const textRef = useRef(text);
const jumblingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const revealTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const mountTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
textRef.current = text;
}, [text]);
useEffect(() => {
if (animateOnMount && !hasMounted) {
setHasMounted(true);
mountTimeoutRef.current = setTimeout(() => {
startJumbleAnimation();
}, mountDelay);
}
return () => {
if (mountTimeoutRef.current) {
clearTimeout(mountTimeoutRef.current);
}
if (jumblingIntervalRef.current) {
clearInterval(jumblingIntervalRef.current);
}
if (animationTimeoutRef.current) {
clearTimeout(animationTimeoutRef.current);
}
if (revealTimeoutRef.current) {
clearTimeout(revealTimeoutRef.current);
}
};
}, [animateOnMount, mountDelay]);
const startJumbleAnimation = () => {
if (jumblingIntervalRef.current) {
clearInterval(jumblingIntervalRef.current);
}
if (animationTimeoutRef.current) {
clearTimeout(animationTimeoutRef.current);
}
if (revealTimeoutRef.current) {
clearTimeout(revealTimeoutRef.current);
}
jumblingIntervalRef.current = setInterval(() => {
setDisplay(scrambleCharacters(textRef.current));
}, jumbleInterval);
animationTimeoutRef.current = setTimeout(() => {
if (jumblingIntervalRef.current) {
clearInterval(jumblingIntervalRef.current);
}
startRevealAnimation();
}, jumbleDuration);
};
const startRevealAnimation = () => {
let currentIndex = 0;
const revealNext = () => {
if (currentIndex >= textRef.current.length) {
setDisplay(textRef.current);
return;
}
const revealed = textRef.current.slice(0, currentIndex + 1);
const remaining = textRef.current.slice(currentIndex + 1);
setDisplay(revealed + scrambleCharacters(remaining));
currentIndex++;
if (currentIndex < textRef.current.length) {
revealTimeoutRef.current = setTimeout(revealNext, revealDuration);
}
};
revealNext();
};
return (
<div
className={cn(
'inline-block whitespace-pre cursor-pointer',
className
)}
onMouseEnter={startJumbleAnimation}
role="button"
tabIndex={0}
>
{display}
</div>
);
};
export default JumbledText;
Now paste it into your project and you’re all set!
Props
Prop | Type | Default | Description |
---|---|---|---|
text | string | — | The string to display and scramble. |
className | string | '' | Extra Tailwind or custom classes on the wrapper. |
jumbleDuration | number | 300 | Total time (ms) spent randomly shuffling characters before reveal starts. |
revealDuration | number | 40 | Delay (ms) between each character reveal during the “unscramble.” |
jumbleInterval | number | 50 | How often (ms) to reshuffle during the jumbling phase. |
animateOnMount | boolean | false | If `true`, runs the scramble/reveal as soon as the component mounts. |