Scramble Text
Scramble Text brings controlled expression to your interface.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/scramble-text.jsonUse it
Scroll horizontally for long lines
"use client";
import {ScrambleText} from '@/components/jez-ui/ui/scramble-text';
export default function Example(){
return <><div className="w-full rounded-xl bg-foreground p-8 text-accent md:p-14"><ScrambleText className="font-display text-4xl font-medium tracking-tight md:text-6xl">Expect the unexpected.</ScrambleText><div className="mt-10 flex justify-between border-t border-current pt-4 text-xs"><span>Order from a little chaos.</span><span>Jez / Motion</span></div></div></>;
}Props & types
Scroll horizontally for long lines
{
children: string;
className?: string;
}Source
View 2 source files
registry/ui/scramble-text.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { useReducedMotion } from "motion/react";
import { cn } from "./utils";
export function ScrambleText({
children,
className,
}: {
children: string;
className?: string;
}) {
const [text, setText] = React.useState(children);
const reduce = useReducedMotion();
const timer = React.useRef<ReturnType<typeof setInterval> | null>(null);
React.useEffect(() => {
setText(children);
return () => {
if (timer.current) clearInterval(timer.current);
};
}, [children, reduce]);
function run() {
if (reduce) return;
if (timer.current) clearInterval(timer.current);
let step = 0;
timer.current = setInterval(() => {
step++;
setText(
Array.from(children)
.map((c, i) =>
i < step / 2 || /\s/.test(c)
? c
: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)],
)
.join(""),
);
if (step >= children.length * 2 && timer.current)
clearInterval(timer.current);
}, 28);
}
let offset = 0;
return (
<span
tabIndex={0}
aria-label={children}
onPointerEnter={run}
onFocus={run}
className={cn("inline-block", className)}
>
{children.split(/(\s+)/).map((word, w) => {
const start = offset;
offset += word.length;
return /\s/.test(word) ? (
<React.Fragment key={w}>{word}</React.Fragment>
) : (
<span
key={w}
className="inline-flex whitespace-nowrap"
aria-hidden="true"
>
{Array.from(word).map((char, i) => (
<span key={i} className="relative inline-block">
<span className="invisible">{char}</span>
<span className="absolute inset-0 text-center">
{text[start + i] ?? char}
</span>
</span>
))}
</span>
);
})}
</span>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
Dependencies
motion@13.2.0 · clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Respect reduced-motion preferences. Decorative effects must not carry essential information. WebGL scenes include a static fallback and only render while visible.
Read the accessibility and performance guide →