Dividir en caracteres, palabras y líneas
Qué estructura de DOM genera SplitText para cada tipo, cómo se nombran los elementos resultantes, y las opciones que controlan la división antes de animar nada.
Animar texto letra a letra parece un problema de animación y en realidad es un problema de manipulación de DOM. Antes de que ningún tween se ejecute hay que convertir un nodo de texto plano en decenas de elementos, cada uno con su caja, su posición y su contribución al layout, sin que el resultado se vea distinto del original ni un píxel. SplitText hace exactamente eso, y conocer la estructura que genera es lo que te permite escribir el CSS que la acompaña en lugar de pelearte con ella.
- Describir la estructura de DOM que produce cada valor de
type. - Elegir entre
SplitText.create()y el constructor, y saber qué devuelve. - Nombrar los elementos generados con las clases incrementales y con
propIndex. - Controlar los casos difíciles con
ignore,wordDelimiter,reduceWhiteSpaceyprepareText.
Tres tipos y su anidamiento
type acepta una lista separada por comas con cualquier combinación de chars, words y lines. El valor por defecto es los tres a la vez, y ese valor por defecto es casi siempre una mala idea: dividir en caracteres un párrafo largo produce centenares de nodos que nunca vas a animar.
La estructura generada se anida en el orden natural. Con type: "lines, words, chars", cada línea contiene palabras y cada palabra contiene caracteres:
<h1 aria-label="Hola mundo">
<div class="linea" aria-hidden="true">
<div class="palabra"><div class="letra">H</div><div class="letra">o</div>…</div>
<div class="palabra"><div class="letra">m</div>…</div>
</div>
</h1>
Con type: "words" solo hay una capa de palabras. Con type: "chars" solo hay caracteres, y ahí aparece el primer problema real: sin agrupación por palabras, el navegador puede partir una palabra por la mitad al final de una línea, porque cada carácter es una caja independiente y no hay nada que le diga que van juntas. Para eso existe smartWrap: true, que envuelve las palabras en un span con white-space: nowrap. Solo tiene efecto si no estás dividiendo también por palabras o líneas, porque en ese caso la agrupación ya existe.
Los espacios no cuentan como caracteres: no obtendrás un elemento por cada espacio.
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
const split = SplitText.create(".titular", { type: "words, chars" });
gsap.from(split.chars, {
yPercent: 120,
autoAlpha: 0,
duration: 0.7,
ease: "power3.out",
stagger: 0.02,
});
La instancia expone los resultados en tres arrays: split.chars, split.words y split.lines. Están en orden de documento, así que un stagger sobre ellos recorre el texto de principio a fin. También existe split.masks cuando usas enmascarado, y split.isSplit para saber si la división está aplicada en este momento.
Las dos formas existen y hacen lo mismo. SplitText.create(destino, config) es la forma recomendada en la documentación actual y la que verás en los ejemplos oficiales; new SplitText(destino, config) es la sintaxis histórica y sigue funcionando. Usa la primera por consistencia con el resto del ecosistema, donde Flip, Observer y Draggable también exponen métodos estáticos de creación.
Nombrar lo que se genera
Por defecto los elementos creados no llevan clase, así que la única forma de seleccionarlos es a través de los arrays de la instancia. En cuanto quieras aplicarles CSS —y con líneas casi siempre querrás— hay que nombrarlos.
charsClass, wordsClass y linesClass aceptan un nombre de clase. Y hay un detalle que ahorra mucho código: si terminas el nombre en "++", SplitText añade además una segunda clase numerada empezando en 1.
const split = SplitText.create(".titular", {
type: "lines, words",
linesClass: "linea++", // clase "linea" y ademas "linea1", "linea2", "linea3"...
wordsClass: "palabra",
});
.linea { overflow: hidden; }
.linea2 { color: var(--acento); }
La alternativa moderna es propIndex: true, que añade a cada elemento una variable CSS con su índice: --char, --word o --line. Eso permite escribir retardos escalonados en CSS puro sin tocar JavaScript, y sobrevive mejor a los cambios porque no depende de generar decenas de clases.
.letra {
animation: entrar 0.5s both;
animation-delay: calc(var(--char) * 30ms);
}
tag cambia el elemento contenedor, que por defecto es div. tag: "span" produce marcado más ligero, pero cuidado: los navegadores no aplican transformaciones —rotación, escala, sesgado— a elementos en línea, así que si usas span tendrás que darles display: inline-block en el CSS o las transformaciones no harán nada.
Los casos difíciles
ignore deja sin dividir a los descendientes que le indiques. El caso típico es un sup, un icono en línea o un abbr que se rompería si lo desmontas letra a letra.
SplitText.create(".precio", { type: "chars", ignore: "sup, .icono" });
deepSlice, activado por defecto, resuelve un problema sutil con elementos anidados. Si dentro de tu titular hay un strong que abarca dos líneas y no se subdivide, ese elemento pertenece a dos líneas a la vez y estira una de ellas verticalmente. Con deepSlice, SplitText lo trocea para que cada parte pertenezca a su línea. Solo tiene efecto al dividir por líneas.
wordDelimiter cambia el separador de palabras, que por defecto es el espacio. Sirve para dividir cosas que no tienen espacios, como una etiqueta con varias palabras pegadas: se insertan caracteres invisibles y se declaran como separador. Desde la versión 3.13 acepta además una forma de objeto con una expresión regular y un texto de sustitución.
// Partir #MeEncantaGSAP en palabras usando un juntador de ancho cero
SplitText.create(".hashtag", {
type: "words",
wordDelimiter: String.fromCharCode(8205),
});
reduceWhiteSpace, activado por defecto, colapsa los espacios consecutivos igual que hace el navegador. Ponerlo a false los conserva, y desde la 3.13 además inserta br donde hubiera saltos de línea, que es lo que hace falta para dividir el contenido de un pre sin destruir su formato.
prepareText es el escape final: una función que recibe cada bloque de texto crudo y el elemento padre, y devuelve el texto modificado justo antes de que SplitText lo trocee. Es la herramienta para idiomas sin espacios entre palabras, donde hay que insertar los separadores a partir de un análisis propio.
Al meter cada carácter en su propia caja se pierde el kerning que el navegador aplicaba entre pares de letras, y el texto se descoloca ligeramente respecto al original. No es un fallo de SplitText, es una consecuencia inevitable de la separación. Se corrige desactivando el kerning en el CSS del elemento que vas a dividir, con font-kerning: none y text-rendering: optimizeSpeed, de modo que el antes y el después coincidan.
Solo dividir lo que vas a animar
Es la regla más importante y la más ignorada. Cada elemento generado es un nodo del DOM con su propio cálculo de estilo, su propia caja y su propia línea en el árbol de layout. Un párrafo de doscientas palabras dividido en caracteres produce del orden de mil doscientos elementos; cinco párrafos así son seis mil nodos que el navegador tiene que estilar y disponer en cada recálculo.
La consecuencia práctica es que la animación por caracteres pertenece a titulares cortos, y la animación por líneas o por palabras a todo lo demás. Un revelado por líneas sobre un párrafo se ve mejor y cuesta cuarenta veces menos.
// Titular: caracteres, porque son doce
SplitText.create("h1", { type: "chars" });
// Parrafo: lineas, porque son cinco y no cuatrocientas
SplitText.create("p.entradilla", { type: "lines" });
Merece la pena entender qué renuncia estás firmando al dividir, porque explica todos los problemas que vendrán después y que se suelen atribuir a fallos de la biblioteca. Un nodo de texto dentro de un elemento no es una caja: es una entrada para el algoritmo de composición de líneas del navegador, que decide dónde romper, cómo justificar, cómo aplicar guiones, cómo tratar los espacios colapsables, cómo aplicar el kerning entre pares concretos y cómo reflowear todo eso cuando cambia el ancho. Ese algoritmo es enormemente sofisticado, lleva treinta años puliéndose y es completamente invisible mientras funciona. En el momento en que metes cada carácter en su propio div, ese algoritmo deja de tener texto que componer y pasa a tener una secuencia de cajas que colocar, y todo lo que hacía por ti desaparece de golpe. Por eso se pierde el kerning, por eso las palabras se parten por la mitad si no las agrupas, por eso text-wrap: balance deja de funcionar, y por eso las líneas dejan de reflowear cuando cambia el ancho: ya no hay líneas, hay contenedores que se calcularon con un ancho concreto y que no saben que ese ancho ha cambiado. La lista entera de opciones de SplitText —smartWrap, deepSlice, reduceWhiteSpace, wordDelimiter, prepareText— es un catálogo de reimplementaciones parciales de cosas que el compositor de líneas hacía gratis. Y de aquí sale la única regla que de verdad importa en este nivel: divide lo mínimo, durante el mínimo tiempo posible, y devuelve el texto a su estado original en cuanto la animación acabe. No es una recomendación de rendimiento; es reconocer que el estado dividido es un estado degradado del documento, y que dejar el texto dividido para siempre es dejar tu página funcionando con el motor de tipografía a medio gas.
- Divide un titular con los tres tipos y examina el HTML resultante en el inspector. Cuenta los nodos.
- Divide solo por caracteres, sin
smartWrap, y estrecha la ventana hasta que una palabra se parta por la mitad. Actívalo y compruébalo. - Aplica
linesClass: "linea++"y usa.linea2en el CSS para colorear la segunda línea. - Sustituye las clases numeradas por
propIndex: truey monta el retardo escalonado en CSS puro. - Mide con el panel de rendimiento el coste de dividir un párrafo largo por caracteres frente a dividirlo por líneas.