Animações escalonadas sem contas de nth-child
Troque regras nth-child e loops JavaScript por sibling-index() para escalonar animações CSS. Veja variações, usos no layout, suporte dos navegadores e alternativas.
Uma única regra, animation-delay: calc((sibling-index() - 1) * 80ms), escalona uma lista de qualquer comprimento. Deixa de precisar de uma regra :nth-child() por item ou de um ciclo JavaScript que defina --i.
A maior parte do código de escalonamento é escrita para cinco itens. Depois, alguém acrescenta um sexto cartão, e esse sexto cartão aparece em fade-in ao mesmo tempo que o primeiro.
Este artigo elimina esse código. Usa um único exemplo do princípio ao fim: uma lista de cartões que aparecem em fade-in, cada um ligeiramente depois do anterior. Vai ver a abordagem antiga, a sua substituta, variantes invertidas e a partir do centro, uma utilização de sibling-count() no layout e uma salvaguarda que torna tudo seguro para produção. O artigo cinge-se ao CSS. Para uma abordagem em React, consulte animações de texto escalonadas com Framer Motion.
Pontos-chave
sibling-index()devolve a posição de um elemento entre os seus irmãos, a contar a partir de 1.sibling-count()devolve o número de elementos filhos do elemento pai, incluindo o próprio elemento na contagem.- Ambas as funções devolvem números simples. Só se tornam um tempo ou um comprimento quando são multiplicadas por uma unidade dentro de
calc(). animation-delay: calc((sibling-index() - 1) * 80ms)substitui uma regra:nth-child()separada para cada cartão, seja qual for o comprimento da lista.- Ambas as funções foram lançadas no Chrome e no Edge 138 em junho de 2025, no Safari 26.2 em dezembro de 2025 e no Firefox 154 em agosto de 2026, o que as torna Baseline recentemente disponíveis (newly available).
- Coloque o atraso dentro de
@supports (top: calc(1px * sibling-index())). Assim, os browsers sem suporte fazem o fade-in de todos os cartões em simultâneo, em vez de falharem.
A forma antiga: escadas de nth-child e propriedades personalizadas inline
Antes de sibling-index(), um escalonamento em CSS obrigava a definir manualmente um atraso por posição. Havia duas formas de o fazer: uma escada de :nth-child() ou uma propriedade personalizada com o índice escrita no markup. A escada usa :nth-child(), que, tal como os seletores de irmãos do CSS que a maioria dos programadores já conhece, seleciona elementos consoante a posição que ocupam entre os seus irmãos:
.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; }
A escada termina no comprimento para que foi escrita. A versão com propriedades personalizadas transfere a contagem para o 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); }
Com listas dinâmicas, acaba normalmente por definir o índice a partir de um script:
document.querySelectorAll('.card').forEach((el, i) => el.style.setProperty('--i', i));
Como é que sibling-index() substitui a escada de nth-child?
sibling-index() substitui toda a escada de :nth-child() por uma única declaração: animation-delay: calc((sibling-index() - 1) * 80ms). A função está definida nas funções de contagem da árvore do CSS Values and Units Level 5. sibling-index() atribui o número 1 ao primeiro elemento irmão, tal como :nth-child(), e ignora os nós de texto e de comentário na contagem.
@keyframes fade-in {
from { opacity: 0; translate: 0 8px; }
}
.card {
animation: fade-in 400ms ease both;
animation-delay: calc((sibling-index() - 1) * 80ms);
}
A função devolve um número inteiro simples. É a multiplicação dentro de calc() que o transforma num tempo.
Subtrai-se 1 para que o primeiro cartão não tenha atraso. Com cinco cartões, os índices de 1 a 5 correspondem a 0, 80, 160, 240 e 320ms. Sem o - 1, cada cartão espera um passo a mais, e a lista fica parada durante 80ms antes de acontecer alguma coisa.
A palavra-chave both na forma abreviada também é importante. Com animation-fill-mode definido como both ou backwards, o primeiro keyframe aplica-se durante o atraso, pelo que os cartões em espera se mantêm invisíveis. Sem ela, os cartões seguintes aparecem com opacidade total e depois saltam para 0 quando a sua animação começa.
Em listas longas, pode limitar o atraso para que o 40.º cartão não espere mais de três segundos:
.card { animation-delay: calc(min(sibling-index() - 1, 10) * 80ms); }
Como inverter ou centrar o escalonamento?
Para inverter um escalonamento com sibling-index(), subtraia o índice a sibling-count(). calc((sibling-count() - sibling-index()) * 80ms) atribui 0ms ao último cartão, pelo que o último cartão é animado primeiro e o primeiro é animado por último. Para um escalonamento a partir do centro, meça a distância de cada cartão ao meio:
.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
);
}
Com cinco cartões, o centro é 3, pelo que os atrasos são de 160, 80, 0, 80 e 160ms. Com um número par de cartões, o centro fica entre dois deles. Quatro cartões dão 120, 40, 40 e 120ms, e os dois cartões do meio começam em simultâneo.
Usar sibling-count() no layout
sibling-count() também funciona como valor de layout. Conta todos os elementos filhos do pai, incluindo o próprio elemento, tal como descrito na referência da MDN. Dividir por este valor dá partes iguais: width: calc(100% / sibling-count()). Eis a mesma lista de cartões, tendo em conta o espaçamento (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); }
Numa simples linha flex, flex: 1 já garante larguras iguais. A fórmula é útil quando o flex não resolve: cartões com posicionamento absoluto ou sobrepostos, ou casos em que a largura alimenta outros cálculos. --progress atribui a cada cartão a sua fração do total, pelo que o cartão 3 de 5 desenha uma barra de 60%.
sibling-count() e sibling-index() contam os irmãos do próprio elemento, não os seus filhos. Se usar sibling-count() no <ul>, obtém o número de filhos do elemento pai da lista (incluindo a própria lista), e não o número de itens <li> que esta contém. Aplique os cálculos baseados no número de filhos aos próprios filhos.
Pôr em produção: suporte dos browsers e uma salvaguarda @supports
sibling-index() e sibling-count() são suportadas pelos três principais motores de renderização, pelo que são Baseline recentemente disponíveis.
| Motor | Versão | Lançamento |
|---|---|---|
| Chrome / Edge | 138 | junho de 2025 |
| Safari (macOS, iOS) | 26.2 | dezembro de 2025 |
| Firefox | 154 | agosto de 2026 |
Continua a haver utilizadores com versões mais antigas, por isso mantenha o fade-in fora da salvaguarda e coloque apenas o escalonamento dentro dela:
.card { animation: fade-in 400ms ease both; }
@supports (top: calc(1px * sibling-index())) {
.card { animation-delay: calc((sibling-index() - 1) * 80ms); }
}
O teste envolve a função num calc() que produz um comprimento e aplica-o a uma propriedade que aceita comprimentos. Um browser que não consiga interpretar essa declaração considera a condição falsa e ignora o bloco. Todos os cartões continuam a aparecer em fade-in, apenas em simultâneo.
Respeitar prefers-reduced-motion
Com prefers-reduced-motion: reduce, definir animation: none nos cartões mostra todos os cartões de imediato no seu estado final:
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}
Conclusão
O escalonamento passa a residir numa única declaração protegida, que funciona para qualquer número de cartões. Remova a escada de :nth-child() e o script que escreve --i, e acrescente o bloco @supports e a regra de movimento reduzido. O seu markup deixa de precisar de atributos de índice. Se grande parte do seu trabalho de animação ainda depende de JavaScript, consulte substituir bibliotecas de animação por APIs web nativas.
Perguntas frequentes
Qual é a diferença entre sibling-index() e a função counter() do CSS?
counter() produz texto, pelo que só é útil dentro da propriedade content. sibling-index() produz um número com o qual pode fazer cálculos, como em calc((sibling-index() - 1) * 80ms). Um valor de counter não pode controlar um animation-delay, uma largura ou um ângulo. sibling-index() pode, e não precisa de regras counter-reset nem counter-increment.
sibling-index() pode ignorar determinados elementos, tal como :nth-child(An+B of S)?
Não. sibling-index() e sibling-count() não recebem argumentos, pelo que não é possível filtrá-las por seletor. Contam todos os elementos irmãos sob o mesmo pai, independentemente da classe. Um título, um divisor ou um elemento wrapper dentro da lista desloca o índice de todos os cartões que se lhe seguem e aumenta a contagem. Mantenha os itens animados como os únicos filhos do respetivo contentor.
sibling-index() pode controlar cores e rotações, além de atrasos?
Sim. O número inteiro funciona em qualquer expressão calc(), pelo que multiplicá-lo por uma unidade dá um ângulo ou um comprimento, por exemplo rotate: calc(sibling-index() * 15deg). Dividir por sibling-count() distribui um valor uniformemente pela lista. Por exemplo, calc(sibling-index() / sibling-count() * 360deg) atribui a cada item a sua própria tonalidade ou a sua própria posição num 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