Animaciones escalonadas sin cálculos con nth-child
Sustituye reglas nth-child y bucles JavaScript por sibling-index() para escalonar animaciones CSS. Consulta variantes, usos en diseño, compatibilidad y alternativas.
Una sola regla, animation-delay: calc((sibling-index() - 1) * 80ms), escalona una lista de cualquier longitud. Ya no hace falta una regla :nth-child() por elemento ni un bucle de JavaScript que asigne --i.
La mayor parte del código de escalonado se escribe pensando en cinco elementos. Luego alguien añade una sexta tarjeta, y esa sexta tarjeta aparece con fundido al mismo tiempo que la primera.
Este artículo elimina ese código. Usa un único ejemplo de principio a fin: una lista de tarjetas que aparecen con fundido, cada una un poco después de la anterior. Verás el enfoque antiguo, su sustituto, las variantes en orden inverso y desde el centro hacia afuera, un uso de sibling-count() para maquetación y una salvaguarda que permite llevarlo a producción con seguridad. El artículo se limita a CSS. Si buscas un enfoque con React, consulta animaciones de texto escalonadas con Framer Motion.
Puntos clave
sibling-index()devuelve la posición de un elemento entre sus hermanos, empezando a contar desde 1.sibling-count()devuelve cuántos elementos hijos tiene el padre, incluido el propio elemento.- Ambas funciones devuelven números simples. Solo se convierten en un tiempo o una longitud cuando las multiplicas por una unidad dentro de
calc(). animation-delay: calc((sibling-index() - 1) * 80ms)sustituye a una regla:nth-child()independiente por cada tarjeta, por larga que sea la lista.- Ambas funciones llegaron a Chrome y Edge 138 en junio de 2025, a Safari 26.2 en diciembre de 2025 y a Firefox 154 en agosto de 2026. Esto las convierte en funcionalidades Baseline newly available (de disponibilidad reciente).
- Coloca el retardo dentro de
@supports (top: calc(1px * sibling-index())). Así, los navegadores sin soporte mostrarán todas las tarjetas con fundido a la vez en lugar de fallar.
El método antiguo: escaleras de nth-child y custom properties en línea
Antes de sibling-index(), escalonar con CSS implicaba fijar a mano un retardo por cada posición. Había dos formas de hacerlo: una escalera de reglas :nth-child() o una custom property de índice escrita en el marcado. La escalera usa :nth-child(), que, al igual que los selectores de hermanos de CSS que la mayoría de los desarrolladores ya conoce, selecciona elementos según su posición entre sus hermanos:
.card { animation: fade-in 400ms ease both; }
.card:nth-child(2) { animation-delay: 80ms; }
.card:nth-child(3) { animation-delay: 160ms; }
.card:nth-child(4) { animation-delay: 240ms; }
.card:nth-child(5) { animation-delay: 320ms; }
La escalera termina en la longitud para la que la escribiste. La versión con custom properties traslada el conteo al HTML:
<li class="card" style="--i: 0">…</li>
<li class="card" style="--i: 1">…</li>
<li class="card" style="--i: 2">…</li>
<li class="card" style="--i: 3">…</li>
.card { animation-delay: calc(var(--i) * 80ms); }
Con listas dinámicas, normalmente acabas asignando el índice desde un script:
document.querySelectorAll('.card').forEach((el, i) => el.style.setProperty('--i', i));
¿Cómo sustituye sibling-index() a la escalera de nth-child?
sibling-index() sustituye toda la escalera de :nth-child() por una sola declaración: animation-delay: calc((sibling-index() - 1) * 80ms). La función está definida en las funciones de conteo del árbol de CSS Values and Units Level 5. sibling-index() asigna el número 1 al primer elemento hermano, igual que :nth-child(), y omite los nodos de texto y de comentario al contar.
@keyframes fade-in {
from { opacity: 0; translate: 0 8px; }
}
.card {
animation: fade-in 400ms ease both;
animation-delay: calc((sibling-index() - 1) * 80ms);
}
La función devuelve un entero sin unidad, así que es la multiplicación dentro de calc() la que lo convierte en un tiempo.
Se resta 1 para que la primera tarjeta no tenga retardo. Con cinco tarjetas, los índices del 1 al 5 se traducen en 0, 80, 160, 240 y 320 ms. Sin el - 1, cada tarjeta espera un paso adicional y la lista permanece inmóvil durante 80 ms antes de que ocurra nada.
La palabra clave both de la forma abreviada también importa. Con animation-fill-mode establecido en both o backwards, el primer fotograma clave se aplica durante el retardo, de modo que las tarjetas en espera permanecen invisibles. Sin ella, las tarjetas posteriores se muestran con opacidad completa y luego saltan a 0 cuando empieza su animación.
En listas largas, puedes limitar el retardo para que la tarjeta número 40 no espere más de tres segundos:
.card { animation-delay: calc(min(sibling-index() - 1, 10) * 80ms); }
¿Cómo se invierte o se centra el escalonado?
Para invertir un escalonado con sibling-index(), resta el índice a sibling-count(). calc((sibling-count() - sibling-index()) * 80ms) asigna 0 ms a la última tarjeta, de modo que la última se anima primero y la primera, al final. Para un escalonado desde el centro hacia afuera, mide la distancia de cada tarjeta respecto al centro:
.card { --centre: calc((sibling-count() + 1) / 2); }
.list--reverse .card {
animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}
.list--centre .card {
animation-delay: calc(
max(sibling-index() - var(--centre), var(--centre) - sibling-index()) * 80ms
);
}
Con cinco tarjetas, el centro es 3, así que los retardos son 160, 80, 0, 80 y 160 ms. Con un número par, el centro queda entre dos tarjetas. Cuatro tarjetas dan 120, 40, 40 y 120 ms, y las dos centrales empiezan a la vez.
Uso de sibling-count() para maquetación
sibling-count() también sirve como valor de maquetación. Cuenta todos los elementos hijos del padre, incluido el propio elemento, tal como describe la referencia de MDN. Al dividir por este valor se obtienen partes iguales: width: calc(100% / sibling-count()). Aquí se aplica a la misma lista de tarjetas, teniendo en cuenta el espacio entre ellas (gap):
.list { display: flex; --gap: 1rem; gap: var(--gap); }
.card {
width: calc((100% - (sibling-count() - 1) * var(--gap)) / sibling-count());
--progress: calc(sibling-index() / sibling-count() * 100%);
}
.card::after { content: ""; display: block; height: 3px; width: var(--progress); }
En una fila flex sencilla, flex: 1 ya proporciona anchos iguales. La fórmula resulta útil cuando flex no basta: tarjetas con posicionamiento absoluto o superpuestas, o casos en los que el ancho alimenta otros cálculos. --progress asigna a cada tarjeta su proporción del total, de modo que la tarjeta 3 de 5 dibuja una barra del 60 %.
sibling-count() y sibling-index() cuentan los hermanos del propio elemento, no sus hijos. Si aplicas sibling-count() al <ul>, obtendrás el número de hijos del padre de la lista (incluida la propia lista), no cuántos elementos <li> contiene. Aplica los cálculos basados en el número de hijos a los propios hijos.
Llevarlo a producción: compatibilidad con navegadores y una salvaguarda con @supports
sibling-index() y sibling-count() están disponibles en los tres motores principales, por lo que se consideran Baseline newly available.
| Motor | Versión | Lanzamiento |
|---|---|---|
| Chrome / Edge | 138 | Junio de 2025 |
| Safari (macOS, iOS) | 26.2 | Diciembre de 2025 |
| Firefox | 154 | Agosto de 2026 |
Todavía hay usuarios con versiones antiguas, así que deja el fundido fuera de la salvaguarda y coloca dentro solo el escalonado:
.card { animation: fade-in 400ms ease both; }
@supports (top: calc(1px * sibling-index())) {
.card { animation-delay: calc((sibling-index() - 1) * 80ms); }
}
La comprobación envuelve la función en un calc() que produce una longitud y la aplica a una propiedad que acepta longitudes. Un navegador que no puede interpretar esa declaración considera falsa la condición y omite el bloque. Todas las tarjetas siguen apareciendo con fundido, solo que al mismo tiempo.
Respetar prefers-reduced-motion
Con prefers-reduced-motion: reduce, establecer animation: none en las tarjetas hace que todas se muestren de inmediato en su estado final:
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}
Conclusión
El escalonado ahora reside en una única declaración protegida que funciona con cualquier número de tarjetas. Elimina la escalera de :nth-child() y el script que escribe --i, añade el bloque @supports y la regla de movimiento reducido, y tu marcado ya no necesitará atributos de índice. Si buena parte de tu trabajo de animación todavía depende de JavaScript, consulta cómo sustituir las librerías de animación por API web nativas.
Preguntas frecuentes
¿Qué diferencia hay entre sibling-index() y la función counter() de CSS?
counter() produce texto, por lo que solo es útil dentro de la propiedad content. sibling-index() produce un número con el que se pueden hacer cálculos, como en calc((sibling-index() - 1) * 80ms). El valor de un contador no puede controlar un animation-delay, un ancho o un ángulo. sibling-index() sí puede, y no necesita reglas counter-reset ni counter-increment.
¿Puede sibling-index() omitir ciertos elementos, como hace :nth-child(An+B of S)?
No. sibling-index() y sibling-count() no aceptan argumentos, así que no se pueden filtrar por selector. Cuentan todos los elementos hermanos bajo el mismo padre, sea cual sea su clase. Un encabezado, un separador o un elemento contenedor dentro de la lista desplaza el índice de todas las tarjetas que vienen después y aumenta el recuento. Mantén los elementos animados como únicos hijos de su contenedor.
¿Puede sibling-index() controlar colores y rotaciones además de retardos?
Sí. El entero funciona en cualquier expresión calc(), así que al multiplicarlo por una unidad se obtiene un ángulo o una longitud, por ejemplo rotate: calc(sibling-index() * 15deg). Al dividirlo por sibling-count() se reparte un valor de forma uniforme a lo largo de la lista. Por ejemplo, calc(sibling-index() / sibling-count() * 360deg) asigna a cada elemento su propio tono o su propia posición alrededor de un círculo.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k