Texto en ola
Cada letra sube y baja con un desfase y el color viaja con la onda; solo anima mientras está a la vista.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useRef, useState, type CSSProperties } from "react";
/** Texto en ola: cada letra sube y baja con un desfase, y el color viaja con la onda. Solo anima mientras está visible. React + TypeScript, sin dependencias. */
export type TextoOlaProps = {
text?: string;
/** Segundos por ciclo de la onda. */
period?: number;
/** Altura de la onda en em. */
amplitude?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gctol-root { display: inline-block; max-width: 100%; color: var(--gctol-fg, #14181f); font: 800 clamp(34px, 7vw, 72px)/1.1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: -.03em; }
.gctol-root *, .gctol-root *::before, .gctol-root *::after { box-sizing: border-box; }
.gctol-c { display: inline-block; white-space: pre; animation: gctol-wave var(--gctol-t, 2.4s) ease-in-out infinite; animation-delay: calc(var(--i) * .09s); animation-play-state: paused; }
.gctol-root[data-live="true"] .gctol-c { animation-play-state: running; }
@keyframes gctol-wave { 0%, 100% { transform: translateY(0); color: var(--gctol-fg, #14181f); } 28% { transform: translateY(calc(var(--gctol-a, .22) * -1em)); color: var(--gctol-hi, #2f5bff); } 56% { transform: translateY(0); color: var(--gctol-fg, #14181f); } }
@media (prefers-reduced-motion: reduce) { .gctol-c { animation: none; } }`;
export function TextoOla({ text = "Ordena tu operación", period = 2.4, amplitude = 0.22, className = "", style }: TextoOlaProps) {
const ref = useRef<HTMLSpanElement>(null);
const [live, setLive] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => setLive(entry.isIntersecting), { threshold: 0.1 });
io.observe(el);
return () => io.disconnect();
}, []);
const vars = { "--gctol-t": `${period}s`, "--gctol-a": amplitude } as CSSProperties;
return (
<span ref={ref} className={`gctol-root ${className}`} style={{ ...vars, ...style }} data-live={live} aria-label={text} role="text">
<style>{CSS}</style>
{Array.from(text).map((char, index) => (
<span key={index} className="gctol-c" aria-hidden="true" style={{ "--i": index } as CSSProperties}>
{char}
</span>
))}
</span>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/texto-olaEjemplo de uso de Texto en ola
Después de copiar texto-ola.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { TextoOla } from "./texto-ola";
export default function Ejemplo() {
return <TextoOla text="Ejemplo" />;
}Propiedades de TextoOla
TextoOla 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. |
period? | number | Segundos por ciclo de la onda. |
amplitude? | number | Altura de la onda en em. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Texto en ola 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 |
|---|---|
--gctol-fg | #14181f |
--gctol-t | 2.4s |
--gctol-a | .22 |
--gctol-hi | #2f5bff |
Accesibilidad y rendimiento
El archivo tiene 46 líneas (2 KB) y define 1 animación con @keyframes. Esto es lo que hace Texto en ola por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Declara roles ARIA para lectores de pantalla: text.
- Se pausa cuando sale de la pantalla para no gastar batería ni CPU.
- Es un componente de cliente de Next.js: incluye la directiva "use client".