Hyper Text
Scrambles through letters and resolves into the real text.
0.9 KB gzippedZero dependenciesReduced-motion safeBase UI & Radix compatible
VELORA UI
Installation
npx shadcn@latest add https://innsite.com.mx/r/hyper-text.jsonSource
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
interface HyperTextProps {
children: string;
/** Milliseconds between scramble frames */
frame?: number;
/** Scramble on hover instead of on mount */
onHover?: boolean;
className?: string;
}
/**
* Scrambles through random letters and resolves into the real text, one
* character at a time. Renders the final text on the server.
*/
export function HyperText({
children,
frame = 40,
onHover = false,
className,
}: HyperTextProps) {
const [display, setDisplay] = useState(children);
const timer = useRef<number | undefined>(undefined);
const scramble = () => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
window.clearInterval(timer.current);
let tick = 0;
timer.current = window.setInterval(() => {
tick += 1;
setDisplay(
children
.split("")
.map((char, i) => {
if (char === " ") return char;
// Each character locks in once the wave has passed it.
if (i < tick / 2) return children[i];
return ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
})
.join("")
);
if (tick / 2 >= children.length) {
window.clearInterval(timer.current);
setDisplay(children);
}
}, frame);
};
useEffect(() => {
if (!onHover) scramble();
return () => window.clearInterval(timer.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children, onHover]);
return (
<span
data-slot="hyper-text"
onMouseEnter={onHover ? scramble : undefined}
className={cn("font-mono tabular-nums", className)}
>
{display}
</span>
);
}

