Ir al contenido
Evaluar mi proyecto
Componentes · Texto y animacionesDisponible

Texto que se descifra

Las letras se mezclan y se fijan de izquierda a derecha.

Demostración interactiva

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

scramble-text.tsx72 líneas
Descargar
"use client";

/**
 * Texto que se descifra: las letras cambian al azar y se fijan de izquierda a derecha. Se activa al montarse, al entrar en pantalla o al pasar el puntero.
 * Usa una fuente monoespaciada para que el ancho no cambie mientras se mezcla. Con movimiento reducido muestra el texto final.
 *
 *   <ScrambleText text="Acceso concedido" trigger="hover" />
 */

import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";

export type ScrambleTextProps = {
  text: string;
  /** Cuándo se descifra: al montar, al entrar en pantalla o al pasar el puntero (también al montar). */
  trigger?: "mount" | "view" | "hover";
  /** Duración en milisegundos. */
  duration?: number;
  /** Caracteres que se usan para mezclar. */
  charset?: string;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcsc-root { display: inline-block; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-variant-ligatures: none; white-space: pre-wrap; }
.gcsc-mixed { opacity: .55; }
`;

export function ScrambleText({ text, trigger = "mount", duration = 1100, charset = "!<>-_\\/[]{}—=+*^?#01", className, style }: ScrambleTextProps) {
  const [frame, setFrame] = useState({ solved: text.length, noise: "" });
  const rootRef = useRef<HTMLSpanElement>(null);
  const raf = useRef(0);

  const run = useCallback(() => {
    cancelAnimationFrame(raf.current);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setFrame({ solved: text.length, noise: "" }); return; }
    const start = performance.now();
    const tick = (now: number) => {
      const progress = Math.min(1, (now - start) / duration);
      const solved = Math.floor(progress * text.length);
      const noise = Array.from(text.slice(solved), (char) => (char === " " ? " " : charset[Math.floor(Math.random() * charset.length)])).join("");
      setFrame({ solved, noise });
      if (progress < 1) raf.current = requestAnimationFrame(tick);
      else setFrame({ solved: text.length, noise: "" });
    };
    raf.current = requestAnimationFrame(tick);
  }, [text, duration, charset]);

  useEffect(() => {
    const element = rootRef.current;
    if (trigger === "view" && element) {
      const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { observer.disconnect(); run(); } }, { threshold: 0.5 });
      observer.observe(element);
      return () => { observer.disconnect(); cancelAnimationFrame(raf.current); };
    }
    const start = requestAnimationFrame(run);
    return () => { cancelAnimationFrame(start); cancelAnimationFrame(raf.current); };
  }, [trigger, run]);

  return (
    <>
      <style>{CSS}</style>
      <span ref={rootRef} className={["gcsc-root", className].filter(Boolean).join(" ")} style={style} aria-label={text} onPointerEnter={trigger === "hover" ? run : undefined}>
        <span aria-hidden="true">
          {text.slice(0, frame.solved)}
          <span className="gcsc-mixed">{frame.noise}</span>
        </span>
      </span>
    </>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/texto-descifrado

Ejemplo de uso de Texto que se descifra

Después de copiar scramble-text.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { ScrambleText } from "./scramble-text";

export default function Ejemplo() {
  return <ScrambleText trigger="mount" text="Ejemplo" />;
}

Propiedades de ScrambleText

ScrambleText acepta 6 propiedades; 1 es obligatoria.

PropiedadTipoDescripción
textstringObligatoria.
trigger?"mount" | "view" | "hover"Cuándo se descifra: al montar, al entrar en pantalla o al pasar el puntero (también al montar).
duration?numberDuración en milisegundos.
charset?stringCaracteres que se usan para mezclar.
className?stringOpcional.
style?CSSPropertiesOpcional.

Variantes disponibles

  • trigger: mount, view, hover

Accesibilidad y rendimiento

El archivo tiene 72 líneas (3 KB) y no necesita @keyframes. Esto es lo que hace Texto que se descifra por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • Se pausa cuando sale de la pantalla para no gastar batería ni CPU.
  • Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

Acerca del producto

Un texto cuyas letras cambian al azar y se van fijando de izquierda a derecha hasta formar la frase. Se activa al montarse, al entrar en pantalla o al pasar el puntero. Usa una fuente monoespaciada para que el ancho no cambie.

Incluye

Un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.

  • Tres disparadores: mount, view y hover
  • Duración y caracteres de mezcla configurables
  • El texto real siempre está en aria-label
  • Con movimiento reducido muestra el texto final

Cómo usarlo

Pasa text y elige trigger. Tamaño y color por style o className; la fuente monoespaciada se cambia desde CSS (.gcsc-root).