Animations échelonnées sans calculs `nth-child`
Remplacez les règles nth-child et les boucles JavaScript par sibling-index() pour échelonner les animations CSS. Découvrez les variantes, usages, compatibilité et solutions de repli.
Une seule règle, animation-delay: calc((sibling-index() - 1) * 80ms), échelonne une liste de n’importe quelle longueur. Vous n’avez plus besoin d’une règle :nth-child() par élément ni d’une boucle JavaScript qui définit --i.
La plupart des animations échelonnées sont écrites pour cinq éléments. Puis quelqu’un ajoute une sixième carte, et celle-ci apparaît en même temps que la première.
Cet article supprime ce code. Il s’appuie sur un seul exemple : une liste de cartes qui apparaissent en fondu, chacune un peu après la précédente. Vous découvrirez l’ancienne approche, sa remplaçante, les variantes inversée et du centre vers l’extérieur, un usage de sibling-count() pour la mise en page, et un garde-fou qui permet de passer en production en toute sécurité. L’article se limite au CSS. Pour une approche React, consultez staggered text animations with Framer Motion.
Points clés
sibling-index()renvoie la position d’un élément parmi ses frères, en commençant à 1.sibling-count()renvoie le nombre d’enfants de type élément du parent, l’élément lui-même compris.- Les deux fonctions renvoient de simples nombres. Ils ne deviennent une durée ou une longueur que lorsque vous les multipliez par une unité dans
calc(). animation-delay: calc((sibling-index() - 1) * 80ms)remplace une règle:nth-child()distincte pour chaque carte, quelle que soit la longueur de la liste.- Les deux fonctions sont disponibles dans Chrome et Edge 138 depuis juin 2025, dans Safari 26.2 depuis décembre 2025 et dans Firefox 154 depuis août 2026. Elles ont donc le statut Baseline « newly available » (nouvellement disponible).
- Placez le délai dans
@supports (top: calc(1px * sibling-index())). Les navigateurs qui ne prennent pas en charge ces fonctions affichent alors toutes les cartes en fondu simultanément, sans rien casser.
L’ancienne méthode : cascades de nth-child et propriétés personnalisées en ligne
Avant sibling-index(), échelonner une animation en CSS impliquait de coder en dur un délai par position. Il existait deux façons de procéder : une cascade de règles :nth-child(), ou une propriété personnalisée d’index écrite dans le balisage. La cascade utilise :nth-child() qui, comme les sélecteurs de frères CSS que la plupart des développeurs connaissent déjà, cible les éléments selon leur position parmi leurs frères :
.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 cascade s’arrête à la longueur pour laquelle vous l’avez écrite. La version avec propriété personnalisée déplace le comptage dans le 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); }
Avec des listes dynamiques, on finit généralement par définir l’index depuis un script :
document.querySelectorAll('.card').forEach((el, i) => el.style.setProperty('--i', i));
Comment sibling-index() remplace-t-elle la cascade de nth-child ?
sibling-index() remplace toute la cascade de règles :nth-child() par une seule déclaration : animation-delay: calc((sibling-index() - 1) * 80ms). La fonction est définie dans les fonctions de comptage d’arborescence de CSS Values and Units Level 5. sibling-index() attribue le numéro 1 au premier frère de type élément, comme le fait :nth-child(), et ignore les nœuds texte et les commentaires dans son comptage.
@keyframes fade-in {
from { opacity: 0; translate: 0 8px; }
}
.card {
animation: fade-in 400ms ease both;
animation-delay: calc((sibling-index() - 1) * 80ms);
}
La fonction renvoie un entier brut : c’est la multiplication dans calc() qui le transforme en durée.
On soustrait 1 pour que la première carte n’ait aucun délai. Avec cinq cartes, les index 1 à 5 correspondent à 0, 80, 160, 240 et 320 ms. Sans le - 1, chaque carte attend un intervalle supplémentaire, et la liste reste figée pendant 80 ms avant que quoi que ce soit ne se produise.
Le mot-clé both dans la propriété raccourcie a aussi son importance. Lorsque animation-fill-mode vaut both ou backwards, la première image clé s’applique pendant le délai, si bien que les cartes en attente restent invisibles. Sans cela, les cartes suivantes s’affichent en opacité totale, puis passent brusquement à 0 au démarrage de leur animation.
Pour les longues listes, vous pouvez plafonner le délai afin que la 40ᵉ carte n’attende pas plus de trois secondes :
.card { animation-delay: calc(min(sibling-index() - 1, 10) * 80ms); }
Comment inverser l’échelonnement ou le faire partir du centre ?
Pour inverser un échelonnement basé sur sibling-index(), soustrayez l’index de sibling-count(). calc((sibling-count() - sibling-index()) * 80ms) attribue 0 ms à la dernière carte : la dernière carte s’anime donc en premier et la première en dernier. Pour un échelonnement du centre vers l’extérieur, mesurez la distance de chaque carte par rapport au milieu :
.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
);
}
Avec cinq cartes, le centre vaut 3, et les délais sont donc de 160, 80, 0, 80 et 160 ms. Avec un nombre pair, le centre tombe entre deux cartes. Quatre cartes donnent 120, 40, 40 et 120 ms, et les deux cartes centrales démarrent ensemble.
Utiliser sibling-count() pour la mise en page
sibling-count() fonctionne aussi comme valeur de mise en page. Elle compte tous les enfants de type élément du parent, l’élément lui-même compris, comme le décrit la documentation de référence MDN. Diviser par cette valeur donne des parts égales : width: calc(100% / sibling-count()). Voici son application à la même liste de cartes, en tenant compte de l’espacement :
.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); }
Dans une simple rangée flex, flex: 1 produit déjà des largeurs égales. La formule devient utile lorsque flex ne suffit pas : cartes en positionnement absolu ou qui se chevauchent, ou cas où la largeur alimente d’autres calculs. --progress attribue à chaque carte sa part du total : la carte 3 sur 5 affiche ainsi une barre de 60 %.
sibling-count() et sibling-index() comptent les frères de l’élément, et non ses enfants. Si vous appliquez sibling-count() au <ul>, vous obtenez le nombre d’enfants du parent de la liste (liste comprise), et non le nombre d’éléments <li> qu’elle contient. Placez donc les calculs basés sur le nombre d’enfants sur les enfants eux-mêmes.
Mise en production : compatibilité navigateurs et garde-fou @supports
sibling-index() et sibling-count() sont disponibles dans les trois principaux moteurs de rendu : elles ont donc le statut Baseline « newly available ».
| Moteur | Version | Date de sortie |
|---|---|---|
| Chrome / Edge | 138 | Juin 2025 |
| Safari (macOS, iOS) | 26.2 | Décembre 2025 |
| Firefox | 154 | Août 2026 |
Certains utilisateurs sont encore sur d’anciennes versions : laissez donc le fondu en dehors du garde-fou et n’y placez que l’échelonnement :
.card { animation: fade-in 400ms ease both; }
@supports (top: calc(1px * sibling-index())) {
.card { animation-delay: calc((sibling-index() - 1) * 80ms); }
}
Le test encapsule la fonction dans un calc() qui produit une longueur, et l’applique à une propriété qui accepte des longueurs. Un navigateur incapable d’analyser cette déclaration considère la condition comme fausse et ignore le bloc. Toutes les cartes apparaissent quand même en fondu, simplement en même temps.
Respecter prefers-reduced-motion
Avec prefers-reduced-motion: reduce, définir animation: none sur les cartes les affiche toutes immédiatement dans leur état final :
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}
Conclusion
L’échelonnement tient désormais en une seule déclaration protégée, qui fonctionne quel que soit le nombre de cartes. Supprimez la cascade de règles :nth-child() et le script qui écrit --i, ajoutez le bloc @supports et la règle pour la réduction des animations : votre balisage n’a plus besoin d’attributs d’index. Si une plus grande partie de vos animations dépend encore de JavaScript, consultez replacing animation libraries with native web APIs.
FAQ
Quelle est la différence entre sibling-index() et la fonction CSS counter() ?
counter() produit du texte : elle n'est donc utile que dans la propriété content. sibling-index() produit un nombre avec lequel vous pouvez effectuer des calculs, comme dans calc((sibling-index() - 1) * 80ms). Une valeur de compteur ne peut pas piloter un animation-delay, une largeur ou un angle. sibling-index() le peut, et elle ne nécessite aucune règle counter-reset ou counter-increment.
sibling-index() peut-elle ignorer certains éléments, comme le fait :nth-child(An+B of S) ?
Non. sibling-index() et sibling-count() ne prennent aucun argument : vous ne pouvez donc pas les filtrer par sélecteur. Elles comptent tous les frères de type élément sous le même parent, quelle que soit leur classe. Un titre, un séparateur ou un élément conteneur à l'intérieur de la liste décale l'index de toutes les cartes qui le suivent et augmente le total. Veillez à ce que les éléments animés soient les seuls enfants de leur conteneur.
sibling-index() peut-elle piloter des couleurs et des rotations en plus des délais ?
Oui. L'entier fonctionne dans n'importe quelle expression calc() : le multiplier par une unité donne un angle ou une longueur, par exemple rotate: calc(sibling-index() * 15deg). Diviser par sibling-count() répartit une valeur uniformément sur la liste. Par exemple, calc(sibling-index() / sibling-count() * 360deg) attribue à chaque élément sa propre teinte ou sa propre position sur un cercle.
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