Scramble Text
Scrambles characters then resolves them into the final text. Install Scramble Text from the VLLNT UI registry with the shadcn CLI.
Scrambles characters then resolves them into the final text. Part of the content family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Scramble Text to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/scramble-text.jsonSource
"use client";
import * as React from "react";
import { cn } from "../../lib/utils";
/** Props for {@link ScrambleText}. */
export type ScrambleTextProps = React.ComponentPropsWithoutRef<"span"> & {
/** Milliseconds for the full resolve. Defaults to `1200`. */
duration?: number;
/** Pool of glyphs used while scrambling. */
scrambleCharacters?: string;
/** Final resolved text. */
text: string;
};
function usePrefersReducedMotion(): boolean {
const [reduced, setReduced] = React.useState(false);
React.useEffect(() => {
if (
typeof window === "undefined" ||
typeof window.matchMedia !== "function"
) {
return;
}
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
const onChange = (): void => {
setReduced(query.matches);
};
onChange();
query.addEventListener("change", onChange);
return () => {
query.removeEventListener("change", onChange);
};
}, []);
return reduced;
}
function scramble(text: string, revealed: number, pool: string): string {
const characters = text.match(/[\s\S]/gu) ?? [];
return characters
.map((character, index) => {
if (index < revealed || character === " ") {
return character;
}
return pool.charAt(Math.floor(Math.random() * pool.length));
})
.join("");
}
const defaultPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/**
* Resolves text left-to-right out of randomized glyphs on mount.
*
* Respects `prefers-reduced-motion`: the final text shows at once.
*
* @example
* ```tsx
* <ScrambleText text="DECRYPTED" />
* ```
*/
export const ScrambleText = ({
className,
duration = 1200,
ref,
scrambleCharacters = defaultPool,
text,
...props
}: ScrambleTextProps & { ref?: React.Ref<HTMLSpanElement> }) => {
const reduced = usePrefersReducedMotion();
const [display, setDisplay] = React.useState(text);
React.useEffect(() => {
if (reduced) {
setDisplay(text);
return;
}
const steps = Math.max(text.length, 1);
const tick = duration / steps;
let revealed = 0;
const timer = setInterval(() => {
revealed += 1;
setDisplay(scramble(text, revealed, scrambleCharacters));
if (revealed >= text.length) {
clearInterval(timer);
}
}, tick);
return () => {
clearInterval(timer);
};
}, [duration, reduced, scrambleCharacters, text]);
return (
<span
aria-label={text}
className={cn("font-mono", className)}
ref={ref}
{...props}
>
<span aria-hidden="true">{display}</span>
</span>
);
};
ScrambleText.displayName = "ScrambleText";
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.