Texto glitch
Título que se descifra al pasar el puntero mientras una copia de color se desplaza por franjas.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
/**
* 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.
npx shadcn@latest add https://www.gassierconsulting.com/r/texto-glitchEjemplo de uso de Texto glitch
Después de copiar texto-glitch.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
text? | string | Opcional. |
ghost? | string | Color de la copia desplazada. |
frameMs? | number | Milisegundos por cuadro del descifrado. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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.
| Variable | Valor 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".
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