Ir al contenido
Evaluar mi proyecto
Componentes · Texto y animacionesDisponible

Texto glitch

Título que se descifra al pasar el puntero mientras una copia de color se desplaza por franjas.

Sistemas críticosPasa el puntero

Código completo

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

texto-glitch.tsx110 líneas
Descargar
/**
 * Título que se desarma al pasar el puntero: las letras se descifran de izquierda a derecha
 * en unos 700 ms y, mientras tanto, una copia de color se desplaza por franjas horizontales
 * como una señal mal sintonizada. Solo ocurre con interacción, nunca en bucle, y el ancho
 * del título no cambia durante el efecto. Lector de pantalla: lee el texto normal. El color
 * de la copia se sobrescribe con --gcgl-ghost.
 *
 *   <TextoGlitch text="Sistemas críticos" />
 */

"use client";

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

export type TextoGlitchProps = {
  text?: string;
  /** Color de la copia desplazada. */
  ghost?: string;
  /** Milisegundos por cuadro del descifrado. */
  frameMs?: number;
  className?: string;
  style?: CSSProperties;
};

const POOL = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789";
const FRAMES = 12;

const CSS = `
.gcgl-root { position: relative; display: inline-block; font: 700 clamp(34px, 5vw, 68px)/1.05 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; letter-spacing: -.035em; cursor: default; }
.gcgl-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.gcgl-hold { visibility: hidden; white-space: pre; }
.gcgl-layer { position: absolute; inset: 0; white-space: pre; pointer-events: none; }
.gcgl-ghost { color: var(--gcgl-ghost, #ff4d3a); clip-path: inset(0 0 100% 0); }
.gcgl-root[data-active="true"] .gcgl-ghost { animation: gcgl-slice .56s steps(1, end) both; }
@keyframes gcgl-slice {
  0% { clip-path: inset(8% 0 72% 0); transform: translateX(-5px); }
  18% { clip-path: inset(56% 0 24% 0); transform: translateX(6px); }
  36% { clip-path: inset(30% 0 48% 0); transform: translateX(-3px); }
  54% { clip-path: inset(70% 0 8% 0); transform: translateX(5px); }
  72% { clip-path: inset(18% 0 62% 0); transform: translateX(-2px); }
  100% { clip-path: inset(0 0 100% 0); transform: none; }
}
@media (prefers-reduced-motion: reduce) { .gcgl-root[data-active="true"] .gcgl-ghost { animation: none; } }
`;

export function TextoGlitch({
  text = "Sistemas críticos",
  ghost = "#ff4d3a",
  frameMs = 55,
  className,
  style,
}: TextoGlitchProps) {
  const [scrambled, setScrambled] = useState<string | null>(null);
  const [active, setActive] = useState(false);
  const [reduced, setReduced] = useState(false);
  const timer = useRef<ReturnType<typeof setInterval> | null>(null);

  useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  useEffect(() => () => { if (timer.current) clearInterval(timer.current); }, []);

  const run = () => {
    if (reduced || active) return;
    setActive(true);
    const chars = Array.from(text);
    let frame = 0;
    timer.current = setInterval(() => {
      frame += 1;
      if (frame >= FRAMES) {
        if (timer.current) clearInterval(timer.current);
        setScrambled(null);
        setActive(false);
        return;
      }
      setScrambled(
        chars
          .map((ch, i) => (!/[\p{L}\p{N}]/u.test(ch) || i / chars.length < frame / FRAMES ? ch : POOL[Math.floor(Math.random() * POOL.length)]))
          .join(""),
      );
    }, Math.max(16, frameMs));
  };

  const shown = scrambled ?? text;

  return (
    <>
      <style>{CSS}</style>
      <span
        className={["gcgl-root", className].filter(Boolean).join(" ")}
        style={{ ...style, ["--gcgl-ghost" as string]: ghost }}
        data-active={active}
        onPointerEnter={run}
      >
        <span className="gcgl-sr">{text}</span>
        <span aria-hidden="true">
          <span className="gcgl-hold">{text}</span>
          <span className="gcgl-layer">{shown}</span>
          <span className="gcgl-layer gcgl-ghost">{shown}</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-glitch

Ejemplo de uso de Texto glitch

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

ejemplo.tsx5 líneas
import { TextoGlitch } from "./texto-glitch";

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

Propiedades de TextoGlitch

TextoGlitch acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
text?stringOpcional.
ghost?stringColor de la copia desplazada.
frameMs?numberMilisegundos por cuadro del descifrado.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Texto glitch con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gcgl-ghost#ff4d3a

Accesibilidad y rendimiento

El archivo tiene 110 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Texto glitch 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-*.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18

Acerca del producto

El efecto ocurre solo con interacción y nunca cambia el ancho del título. El lector de pantalla lee el texto normal.

Incluye

Un solo archivo React + TypeScript con estilos dentro. Sin Tailwind ni dependencias externas.

  • Descifrado de izquierda a derecha
  • Copia desplazada por franjas
  • Ancho estable durante el efecto
  • Color de la copia configurable
  • Respeta prefers-reduced-motion