Ir al contenido
Evaluar mi proyecto
Componentes · Texto y animacionesDisponible

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.

texto-ola.tsx46 líneas
Descargar
"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.

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

Ejemplo de uso de Texto en ola

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

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
text?stringOpcional.
period?numberSegundos por ciclo de la onda.
amplitude?numberAltura de la onda en em.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

VariableValor por defecto
--gctol-fg#14181f
--gctol-t2.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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)