Command Palette

Search for a command to run...

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

KINESIS
is 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

PropTypeDefaultDescription
textstringThe string to display and scramble.
classNamestring''Extra Tailwind or custom classes on the wrapper.
jumbleDurationnumber300Total time (ms) spent randomly shuffling characters before reveal starts.
revealDurationnumber40Delay (ms) between each character reveal during the “unscramble.”
jumbleIntervalnumber50How often (ms) to reshuffle during the jumbling phase.
animateOnMountbooleanfalseIf `true`, runs the scramble/reveal as soon as the component mounts.