Customize
Usage
import { TextScramble } from "@/components/motionkit/text-scramble";
<TextScramble text="DECRYPTING MOTION" />Motion
The exact curves this component runs, plotted with Motion's own maths.
Decode
On mount, and on hover
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/text-scramble.tsx.
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useReducedMotion } from "motion/react";
type TextScrambleProps = {
text: string;
/** Milliseconds between frames. Lower is faster. */
speed?: number;
/** Characters used while scrambling. */
characters?: string;
/** Which characters settle first. */
direction?: "start" | "end" | "center" | "random";
/** When the first scramble runs. */
trigger?: "mount" | "view" | "hover";
/** Re-run the effect when the pointer enters. */
scrambleOnHover?: boolean;
className?: string;
};
/** The frame order each character settles in, so the text resolves from the chosen direction. */
function settleOrder(length: number, direction: NonNullable<TextScrambleProps["direction"]>) {
const indexes = Array.from({ length }, (_, index) => index);
if (direction === "end") return indexes.map((index) => length - 1 - index);
if (direction === "center") return indexes.map((index) => Math.round(Math.abs(index - (length - 1) / 2)));
if (direction === "random") {
// shuffled ranks, so every character still settles exactly once
const ranks = [...indexes];
for (let i = ranks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[ranks[i], ranks[j]] = [ranks[j], ranks[i]];
}
return ranks;
}
return indexes;
}
export function TextScramble({
text,
speed = 40,
characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*+=",
direction = "start",
trigger = "mount",
scrambleOnHover = true,
className = "",
}: TextScrambleProps) {
const reduceMotion = useReducedMotion();
const [display, setDisplay] = useState(text);
const timer = useRef<number | undefined>(undefined);
const ref = useRef<HTMLSpanElement>(null);
const scramble = useCallback(() => {
window.clearInterval(timer.current);
if (reduceMotion) return;
const order = settleOrder(text.length, direction);
const last = Math.max(...order, 0);
let frame = 0;
// each character settles a little after the one before it in the chosen order
timer.current = window.setInterval(() => {
frame++;
setDisplay(
text
.split("")
.map((char, index) => {
if (char === " " || order[index] < frame / 2) return char;
return characters[Math.floor(Math.random() * characters.length)];
})
.join(""),
);
if (frame / 2 > last) window.clearInterval(timer.current);
}, speed);
}, [text, speed, characters, direction, reduceMotion]);
useEffect(() => {
if (trigger === "mount") scramble();
if (trigger === "view" && ref.current) {
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
scramble();
observer.disconnect();
});
observer.observe(ref.current);
return () => {
observer.disconnect();
window.clearInterval(timer.current);
};
}
return () => window.clearInterval(timer.current);
}, [scramble, trigger]);
const onHover = trigger === "hover" || scrambleOnHover;
return (
<span ref={ref} onPointerEnter={onHover ? scramble : undefined} className={`inline-block font-mono ${className}`}>
{/* screen readers get the real text once, never the scrambled frames */}
<span className="sr-only">{text}</span>
<span aria-hidden>{reduceMotion ? text : display}</span>
</span>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | The final text. |
| speed | number | 40 | Milliseconds between frames. Lower is faster. |
| characters | string | "A–Z0–9#%&*+=" | Characters used while scrambling. |
| direction | "start" | "end" | "center" | "random" | "start" | Which characters settle first. |
| trigger | "mount" | "view" | "hover" | "mount" | When the first scramble runs: on load, when scrolled into view, or on first hover. |
| scrambleOnHover | boolean | true | Re-run the effect when the pointer enters. |
Come closer
Variable Proximity
Letters near the cursor swell to a heavier weight and ease back as it moves on, using a variable font.
Pull me
Magnetic Letters
Each letter leans toward the cursor and springs home, like a magnetic button for every character.
Shuffle Text
On hover or focus, the letters swap places a few times and slide home. Made for links and buttons.