Command Palette

Search for a command to run...

Text Scramble

An interactive collection of stacked headings that reveal individual content sections with a click.

Text Scramble

Installation

Install the following dependencies:

pnpm add motion

Create a file at components/motion/text-scramble.tsx and paste the given code.

"use client";
import { type JSX, useEffect, useState } from "react";
import { motion, MotionProps } from "motion/react";
 
export type TextScrambleProps = {
    children: string;
    duration?: number;
    speed?: number;
    characterSet?: string;
    as?: React.ElementType;
    className?: string;
    trigger?: boolean;
    onScrambleComplete?: () => void;
} & MotionProps;
 
const defaultChars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 
export function TextScramble({
    children,
    duration = 0.8,
    speed = 0.04,
    characterSet = defaultChars,
    className,
    as: Component = "p",
    trigger = true,
    onScrambleComplete,
    ...props
}: TextScrambleProps) {
    const MotionComponent = motion.create(
        Component as keyof JSX.IntrinsicElements,
    );
    const [displayText, setDisplayText] = useState(children);
    const [isAnimating, setIsAnimating] = useState(false);
    const text = children;
 
    const scramble = async () => {
        if (isAnimating) return;
        setIsAnimating(true);
 
        const steps = duration / speed;
        let step = 0;
 
        const interval = setInterval(() => {
            let scrambled = "";
            const progress = step / steps;
 
            for (let i = 0; i < text.length; i++) {
                if (text[i] === " ") {
                    scrambled += " ";
                    continue;
                }
 
                if (progress * text.length > i) {
                    scrambled += text[i];
                } else {
                    scrambled +=
                        characterSet[
                            Math.floor(Math.random() * characterSet.length)
                        ];
                }
            }
 
            setDisplayText(scrambled);
            step++;
 
            if (step > steps) {
                clearInterval(interval);
                setDisplayText(text);
                setIsAnimating(false);
                onScrambleComplete?.();
            }
        }, speed * 1000);
    };
 
    useEffect(() => {
        if (!trigger) return;
 
        scramble();
    }, [trigger]);
 
    return (
        <MotionComponent className={className} {...props}>
            {displayText}
        </MotionComponent>
    );
}

Update the import paths to match your project setup.