Texto que se ilumina palabra a palabra
Cada palabra pasa de gris tenue a tinta según el scroll; el texto completo es legible para lectores de pantalla desde el inicio.
Desplázate ↓
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useRef, type CSSProperties } from "react";
/** Párrafo que se ilumina palabra a palabra con el scroll: cada palabra pasa de gris tenue a tinta según su posición en pantalla. El texto completo es legible para lectores de pantalla desde el inicio. Autocontenido en su panel. React + TypeScript, sin dependencias. */
export type ScrollPalabrasProps = {
text?: string;
height?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcspa-root { box-sizing: border-box; position: relative; width: 100%; height: var(--h, 460px); overflow-y: auto; overflow-x: hidden; background: #faf8f3; color: #17150f; font: 400 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; scrollbar-width: thin; outline: 0; }
.gcspa-root *, .gcspa-root *::before, .gcspa-root *::after { box-sizing: border-box; }
.gcspa-root:focus-visible { box-shadow: inset 0 0 0 2px #17150f; }
.gcspa-track { height: calc(var(--h, 460px) * 2.4); }
.gcspa-stage { position: sticky; top: 0; display: grid; align-content: center; height: var(--h, 460px); padding: 0 clamp(20px, 6vw, 72px); }
.gcspa-p { margin: 0; max-width: 24ch; font: 500 clamp(30px, 5.4vw, 64px)/1.12 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.03em; }
.gcspa-w { display: inline; opacity: calc(.14 + var(--k, 0) * .86); transition: opacity .15s linear; }
.gcspa-hint { position: absolute; left: clamp(20px, 6vw, 72px); bottom: 14px; font-size: 12px; color: #8c8573; }
@media (prefers-reduced-motion: reduce) { .gcspa-w { opacity: 1 !important; transition: none; } }`;
const DEFAULT = "No vendemos horas de programación. Entendemos cómo trabaja tu empresa y construimos el software que le queda a la medida, para que tu operación no tenga que parecerse a ningún sistema genérico.";
export function ScrollPalabras({ text = DEFAULT, height = 460, className = "", style }: ScrollPalabrasProps) {
const ref = useRef<HTMLDivElement>(null);
const words = text.split(" ");
useEffect(() => {
const el = ref.current;
if (!el) return;
let raf = 0;
const update = () => {
raf = 0;
const max = el.scrollHeight - el.clientHeight;
const p = max > 0 ? el.scrollTop / max : 0;
const spans = el.querySelectorAll<HTMLElement>(".gcspa-w");
const n = spans.length;
spans.forEach((span, i) => {
const start = (i / n) * 0.8;
const k = Math.max(0, Math.min(1, (p - start) / 0.06));
span.style.setProperty("--k", String(k));
});
};
const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
update();
el.addEventListener("scroll", onScroll, { passive: true });
return () => { el.removeEventListener("scroll", onScroll); cancelAnimationFrame(raf); };
}, [text]);
return (
<div ref={ref} className={`gcspa-root ${className}`} style={{ "--h": `${height}px`, ...style } as CSSProperties} tabIndex={0} role="region" aria-label="Texto que se ilumina al desplazar">
<style>{CSS}</style>
<div className="gcspa-track">
<div className="gcspa-stage">
<p className="gcspa-p" aria-label={text}>
{words.map((word, i) => (
<span key={i} className="gcspa-w" aria-hidden="true">
{word}{" "}
</span>
))}
</p>
<span className="gcspa-hint">Desplázate ↓</span>
</div>
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/scroll-palabrasEjemplo de uso de Texto que se ilumina palabra a palabra
Después de copiar scroll-palabras.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { ScrollPalabras } from "./scroll-palabras";
export default function Ejemplo() {
return <ScrollPalabras text="Ejemplo" />;
}Propiedades de ScrollPalabras
ScrollPalabras acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
text? | string | Opcional. |
height? | number | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Texto que se ilumina palabra a palabra 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 |
|---|---|
--h | 460px |
--k | 0 |
Accesibilidad y rendimiento
El archivo tiene 70 líneas (3 KB) y no necesita @keyframes. Esto es lo que hace Texto que se ilumina palabra a palabra por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Declara roles ARIA para lectores de pantalla: region.
- Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
- Es un componente de cliente de Next.js: incluye la directiva "use client".