Gestaffelte Animationen ohne nth-child-Rechnerei
Ersetzen Sie nth-child-Regeln und JavaScript-Schleifen durch sibling-index() für gestaffelte CSS-Animationen. Erfahren Sie mehr über Varianten, Layout, Support und Fallbacks.
Eine einzige Regel, animation-delay: calc((sibling-index() - 1) * 80ms), staffelt eine Liste beliebiger Länge. Sie brauchen also weder eine eigene :nth-child()-Regel pro Element noch eine JavaScript-Schleife, die --i setzt.
Der meiste Stagger-Code ist für fünf Elemente geschrieben. Dann fügt jemand eine sechste Karte hinzu, und diese blendet sich gleichzeitig mit der ersten ein.
Dieser Artikel ersetzt diesen Code. Er verwendet durchgehend ein Beispiel: eine Liste von Karten, die nacheinander mit leichtem Versatz eingeblendet werden. Sie sehen den bisherigen Ansatz, den Ersatz, Varianten in umgekehrter Reihenfolge und von der Mitte nach außen, einen Layout-Einsatz für sibling-count() sowie eine Absicherung, die den Einsatz in Produktion sicher macht. Der Artikel beschränkt sich auf CSS. Einen React-Ansatz finden Sie unter gestaffelte Textanimationen mit Framer Motion.
Das Wichtigste in Kürze
sibling-index()liefert die Position eines Elements unter seinen Geschwistern, beginnend bei 1.sibling-count()liefert die Anzahl der Kindelemente des Elternelements, wobei das Element selbst mitgezählt wird.- Beide Funktionen geben reine Zahlen zurück. Zu einer Zeit- oder Längenangabe werden sie erst, wenn Sie sie innerhalb von
calc()mit einer Einheit multiplizieren. animation-delay: calc((sibling-index() - 1) * 80ms)ersetzt eine separate:nth-child()-Regel für jede Karte, egal wie lang die Liste wird.- Beide Funktionen wurden in Chrome und Edge 138 im Juni 2025, in Safari 26.2 im Dezember 2025 und in Firefox 154 im August 2026 ausgeliefert. Damit gelten sie als Baseline „Newly available”.
- Setzen Sie die Verzögerung in
@supports (top: calc(1px * sibling-index())). Browser ohne Unterstützung blenden dann alle Karten gleichzeitig ein, statt fehlerhaft darzustellen.
Der bisherige Weg: nth-child-Treppen und Inline-Custom-Properties
Vor sibling-index() bedeutete eine Staffelung in CSS, für jede Position eine Verzögerung fest zu codieren. Dafür gab es zwei Wege: eine :nth-child()-Treppe oder eine Index-Custom-Property direkt im Markup. Die Treppe nutzt :nth-child(), das – wie die CSS-Geschwisterselektoren, die die meisten Entwickler bereits kennen – Elemente anhand ihrer Position unter ihren Geschwistern auswählt:
.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; }
Die Treppe endet bei genau der Länge, für die Sie sie geschrieben haben. Die Custom-Property-Variante verlagert das Zählen ins 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); }
Bei dynamischen Listen setzen Sie den Index meist per Skript:
document.querySelectorAll('.card').forEach((el, i) => el.style.setProperty('--i', i));
Wie ersetzt sibling-index() die nth-child-Treppe?
sibling-index() ersetzt die gesamte :nth-child()-Treppe durch eine einzige Deklaration: animation-delay: calc((sibling-index() - 1) * 80ms). Die Funktion ist in den Tree-Counting-Funktionen von CSS Values and Units Level 5 definiert. sibling-index() nummeriert das erste Geschwisterelement mit 1 – genau wie :nth-child() – und überspringt beim Zählen Text- und Kommentarknoten.
@keyframes fade-in {
from { opacity: 0; translate: 0 8px; }
}
.card {
animation: fade-in 400ms ease both;
animation-delay: calc((sibling-index() - 1) * 80ms);
}
Die Funktion gibt eine reine Ganzzahl zurück; erst die Multiplikation innerhalb von calc() macht daraus eine Zeitangabe.
Sie ziehen 1 ab, damit die erste Karte keine Verzögerung hat. Bei fünf Karten ergeben die Indizes 1 bis 5 die Werte 0, 80, 160, 240 und 320 ms. Ohne das - 1 wartet jede Karte einen zusätzlichen Schritt, und die Liste steht 80 ms still, bevor überhaupt etwas passiert.
Auch das Schlüsselwort both in der Kurzschreibweise ist wichtig. Ist animation-fill-mode auf both oder backwards gesetzt, gilt der erste Keyframe bereits während der Verzögerung, sodass wartende Karten unsichtbar bleiben. Ohne diese Einstellung erscheinen spätere Karten zunächst mit voller Deckkraft und springen dann auf 0, sobald ihre Animation startet.
Bei langen Listen können Sie die Verzögerung begrenzen, damit die 40. Karte nicht über drei Sekunden wartet:
.card { animation-delay: calc(min(sibling-index() - 1, 10) * 80ms); }
Wie kehren Sie die Staffelung um oder starten sie in der Mitte?
Um eine sibling-index()-Staffelung umzukehren, ziehen Sie den Index von sibling-count() ab. calc((sibling-count() - sibling-index()) * 80ms) gibt der letzten Karte 0 ms, sodass die letzte Karte zuerst und die erste zuletzt animiert wird. Für eine Staffelung von der Mitte nach außen messen Sie den Abstand jeder Karte zur Mitte:
.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
);
}
Bei fünf Karten liegt die Mitte bei 3, die Verzögerungen betragen also 160, 80, 0, 80 und 160 ms. Bei einer geraden Anzahl liegt die Mitte zwischen zwei Karten. Vier Karten ergeben 120, 40, 40 und 120 ms, und die beiden mittleren Karten starten gleichzeitig.
sibling-count() für das Layout nutzen
sibling-count() eignet sich auch als Layout-Wert. Die Funktion zählt jedes Kindelement des Elternelements einschließlich des Elements selbst, wie in der MDN-Referenz beschrieben. Eine Division dadurch ergibt gleich große Anteile: width: calc(100% / sibling-count()). Hier das Ganze an derselben Kartenliste, unter Berücksichtigung des Abstands:
.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); }
In einer einfachen Flex-Zeile sorgt flex: 1 bereits für gleiche Breiten. Die Formel ist dann nützlich, wenn Flexbox an seine Grenzen stößt: bei absolut positionierten oder sich überlappenden Karten oder wenn die Breite in weitere Berechnungen einfließt. --progress gibt jeder Karte ihren Anteil an der Gesamtzahl, sodass Karte 3 von 5 einen Balken von 60 % zeichnet.
sibling-count() und sibling-index() zählen die Geschwister des Elements selbst, nicht seine Kinder. Setzen Sie sibling-count() auf die <ul>, erhalten Sie die Anzahl der Kinder des Elternelements der Liste (die Liste eingeschlossen) – nicht die Anzahl der enthaltenen <li>-Elemente. Berechnungen, die auf der Anzahl der Kinder basieren, gehören daher auf die Kindelemente.
Produktiver Einsatz: Browser-Unterstützung und eine @supports-Absicherung
sibling-index() und sibling-count() werden in allen drei großen Engines unterstützt und sind damit Baseline „Newly available”.
| Engine | Version | Veröffentlicht |
|---|---|---|
| Chrome / Edge | 138 | Juni 2025 |
| Safari (macOS, iOS) | 26.2 | Dezember 2025 |
| Firefox | 154 | August 2026 |
Es gibt nach wie vor Nutzer mit älteren Versionen. Lassen Sie das Einblenden daher außerhalb der Absicherung und setzen Sie nur die Staffelung hinein:
.card { animation: fade-in 400ms ease both; }
@supports (top: calc(1px * sibling-index())) {
.card { animation-delay: calc((sibling-index() - 1) * 80ms); }
}
Der Test verpackt die Funktion in ein calc(), das eine Länge ergibt, und wendet es auf eine Eigenschaft an, die Längen akzeptiert. Ein Browser, der diese Deklaration nicht parsen kann, wertet die Bedingung als falsch und überspringt den Block. Alle Karten werden trotzdem eingeblendet – nur eben gleichzeitig.
prefers-reduced-motion berücksichtigen
Bei prefers-reduced-motion: reduce sorgt animation: none auf den Karten dafür, dass alle Karten sofort in ihrem Endzustand angezeigt werden:
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}
Fazit
Die Staffelung steckt jetzt in einer einzigen, abgesicherten Deklaration, die für beliebig viele Karten funktioniert. Entfernen Sie die :nth-child()-Treppe und das Skript, das --i schreibt, ergänzen Sie den @supports-Block und die Reduced-Motion-Regel – und Ihr Markup benötigt keine Index-Attribute mehr. Wenn weitere Teile Ihrer Animationen noch von JavaScript abhängen, lesen Sie Animationsbibliotheken durch native Web-APIs ersetzen.
FAQs
Was ist der Unterschied zwischen sibling-index() und der CSS-Funktion counter()?
counter() erzeugt Text und ist daher nur innerhalb der Eigenschaft content nützlich. sibling-index() erzeugt eine Zahl, mit der Sie rechnen können, etwa in calc((sibling-index() - 1) * 80ms). Ein Counter-Wert kann weder animation-delay noch eine Breite oder einen Winkel steuern. sibling-index() kann das – und benötigt keine counter-reset- oder counter-increment-Regeln.
Kann sibling-index() bestimmte Elemente überspringen, wie es :nth-child(An+B of S) tut?
Nein. sibling-index() und sibling-count() nehmen keine Argumente entgegen, daher lassen sie sich nicht per Selektor filtern. Sie zählen jedes Geschwisterelement unter demselben Elternelement, unabhängig von seiner Klasse. Eine Überschrift, ein Trennelement oder ein Wrapper-Element innerhalb der Liste verschiebt den Index jeder nachfolgenden Karte und erhöht die Anzahl. Sorgen Sie dafür, dass die animierten Elemente die einzigen Kinder ihres Containers sind.
Kann sibling-index() neben Verzögerungen auch Farben und Rotationen steuern?
Ja. Die Ganzzahl funktioniert in jedem calc()-Ausdruck, sodass eine Multiplikation mit einer Einheit einen Winkel oder eine Länge ergibt, zum Beispiel rotate: calc(sibling-index() * 15deg). Eine Division durch sibling-count() verteilt einen Wert gleichmäßig über die Liste. So gibt calc(sibling-index() / sibling-count() * 360deg) jedem Element einen eigenen Farbton oder eine eigene Position auf einem Kreis.
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