Wie viele Elemente in einer Liste stehen, entscheidet die Redaktion, nicht das Stylesheet. Jede CSS-Regel, die eine feste Anzahl voraussetzt, ist deshalb eine Regel auf Abruf – sie hält genau so lange, bis jemand etwas hinzufügt.
Zehn Regeln für zehn Elemente
Auf einer Kundenseite blenden sich die Leistungskacheln beim Scrollen nacheinander
ein, jede einen Sekundenbruchteil nach der vorigen. Gebaut war das mit zehn
:nth-child-Regeln, eine je Kachel, jede mit ihrer eigenen
Verzögerung. Ein knappes Jahr später legte die Redaktion zwei weitere Leistungen
an – und die letzten beiden Kacheln erschienen ohne Verzögerung, gleichzeitig mit
der zehnten. Kein Fehler im Log, keine kaputte Seite, nur ein Effekt, der hinten
abbricht.
Die üblichen Auswege sind alle schon einmal gebaut worden. Man verlängert die
Regelliste und verschiebt das Problem um zwei Einträge. Man schreibt die Position
ins Markup, style="--i: 11" an jeder Kachel, und muss sie bei jeder
Umsortierung im Backend neu erzeugen. Oder man lässt JavaScript nach dem Laden
durchzählen und hängt damit die Gestaltung an ein Skript, das auch scheitern kann.
Alle drei arbeiten um dieselbe Lücke herum. CSS konnte die Position eines Elements
bisher nur treffen, nicht ausrechnen.
:nth-child(3) sagt „das dritte“, aber es gab keinen Weg, an die 3
heranzukommen und mit ihr weiterzurechnen.
Zwei Funktionen ohne Argument
Genau diese Lücke schließen zwei Funktionen, die beide ohne Klammerinhalt auskommen:
- sibling-index() liefert die Position des Elements unter seinen Geschwistern. Gezählt wird ab 1, und zwar genau wie bei :nth-child() – alle Element-Geschwister, unabhängig vom Tag.
- sibling-count() liefert die Anzahl der direkten Kinder des Elternelements, das Element selbst eingerechnet. In einer Liste mit sechs Einträgen bekommt der letzte also 6 und 6.
Entscheidend ist der Rückgabetyp. Beide geben ein <integer>
zurück, keine Zeichenkette. counter() kennt die Position zwar auch,
liefert sie aber als String und ist damit auf content beschränkt – man
kann sie hinschreiben, aber nicht mit ihr rechnen. Eine Zahl dagegen darf überall
dorthin, wo calc() erlaubt ist:
li {
/* 1 für das erste Kind, 2 für das zweite, und so weiter.
Ohne Parameter – die Funktion kennt ihren Kontext selbst. */
padding-left: calc(sibling-index() * 1.2rem);
}
Gestaffelt einblenden
Der Fall aus der Einleitung schrumpft damit auf eine einzige Regel. Der Abzug von 1 ist dabei kein Schönheitsfehler, sondern Absicht: Die Zählung beginnt bei 1, das erste Element soll aber ohne Wartezeit starten.
.zw-leistungen li {
animation: einblenden .4s ease-out backwards;
animation-delay: calc((sibling-index() - 1) * 80ms);
}
@keyframes einblenden {
from { opacity: 0; translate: 0 1rem; }
}
/* Die Staffelung ist Schmuck. Wer Bewegung abbestellt hat,
bekommt den Endzustand sofort statt zwölf Mal verzögert. */
@media (prefers-reduced-motion: reduce) {
.zw-leistungen li { animation: none; }
}
Ob dort zehn Kacheln stehen oder dreiundzwanzig, spielt für die Regel keine Rolle mehr. Die Demo darunter zeigt beides, was die zwei Funktionen können: die Balken pulsieren versetzt, weil jeder seine Verzögerung aus seiner Position bezieht, und sie sind unterschiedlich eingefärbt, weil jeder seine Position ins Verhältnis zur Gesamtzahl setzt.
calc((sibling-index() - 1) * 120ms)
calc(sibling-count() - sibling-index())
Farbe aus der Position
Aus beiden Zahlen zusammen wird ein Anteil, und ein Anteil ist ein Prozentwert – damit steuert die Position direkt eine Farbmischung:
.zw-legende li {
/* Beim letzten Kind ist der Anteil genau 100%. */
--anteil: calc(sibling-index() / sibling-count() * 100%);
background: color-mix(in oklch, #3b82f6, #e6195d var(--anteil));
}
.zw-legende--gespreizt li {
/* Soll das erste Kind bei 0% starten statt bei 1/n, wird
der Nenner um eins kleiner. Achtung bei genau einem Kind:
dann steht dort eine Division durch null. */
--anteil: calc(
(sibling-index() - 1) / (sibling-count() - 1) * 100%
);
}
Welche Schriftfarbe auf diesen Flächen noch lesbar ist, musst du dabei nicht selbst ausrechnen – das übernimmt contrast-color(), und zwar für jeden Mischwert einzeln.
Stapel und Fächer
Der zweite Einsatzbereich hat mit Bewegung nichts zu tun. Überlappende Avatare stapeln sich von Haus aus in Dokumentreihenfolge, das letzte Bild liegt oben. Gewünscht ist meist das Gegenteil, und die Umkehrung ist eine Subtraktion:
.zw-avatare { display: flex; }
.zw-avatare > * {
margin-left: -1.4rem;
/* Beim ersten Kind der höchste Wert, beim letzten 0. */
z-index: calc(sibling-count() - sibling-index());
}
.zw-karten > * {
/* Fächert um die Mitte auf, gleichgültig wie viele Karten
es sind: Bei fünf steht die dritte gerade. */
rotate: calc(
(sibling-index() - (sibling-count() + 1) / 2) * 8deg
);
}
Wo die Zahl nicht ankommt
Eine Stolperfalle kostet erfahrungsgemäß am meisten Zeit, weil das Ergebnis plausibel aussieht und trotzdem falsch ist.
Der Rest ist schnell aufgezählt:
- Gezählt wird der DOM, nicht die Darstellung. Ein Geschwisterelement mit display: none zählt mit und lässt eine Lücke in der Staffelung; Textknoten und Kommentare zählen nicht.
- Die Funktionen liefern Werte, keine Bedingungen. In Media Queries oder Container Queries lässt sich damit nichts abfragen – dort gibt es kein Element, auf das sich die Zählung beziehen könnte.
- Als Selektor-Ersatz taugen sie nicht. :nth-child() bleibt zuständig, wenn eine Regel nur für bestimmte Positionen gelten soll; die neuen Funktionen sind für Werte da, die sich mit der Position ändern.
- Eine Division durch sibling-count() - 1 verwirft der Browser, sobald das Elternelement nur ein Kind hat. Ein Ersatzwert in var() fängt genau diesen Fall ab.
Einbauen, ohne etwas zu brechen
Beide Funktionen sind seit August 2026 Baseline, und das heißt auch hier: in den aktuellen Browsern angekommen, aber noch nicht auf allen Geräten deiner Besucher.
- Chrome und Edge liefern beide Funktionen seit Version 138 im Juni 2025 aus.
- Safari seit Version 26.2 im Dezember 2025, auf dem iPhone in derselben Version.
- Firefox seit Version 154 vom 18. August 2026 – das ist der Tag, an dem die Funktionen Baseline wurden.
Der Fallback braucht kein @supports. Es genügt, dieselbe Eigenschaft
zweimal zu deklarieren: Ein Browser, der sibling-index() nicht kennt,
verwirft die zweite Zeile beim Parsen und behält die erste. Ein Browser, der sie
kennt, nimmt die zweite, weil sie später steht.
.zw-leistungen li {
animation: einblenden .4s ease-out backwards;
/* Zeile 1: greift überall. Der Ersatzwert 0 bedeutet
"alle gleichzeitig" – unspektakulär, aber nicht kaputt.
Steht style="--i: 3" im Markup, staffelt auch diese Zeile. */
animation-delay: calc(var(--i, 0) * 80ms);
/* Zeile 2: gewinnt, wo es die Funktion gibt – und macht das
Attribut im Markup überflüssig. */
animation-delay: calc((sibling-index() - 1) * 80ms);
}
Dieses Muster hat einen angenehmen Nebeneffekt für den Umbau eines Bestands: Die
--i-Attribute im HTML dürfen erst einmal stehen bleiben. Sie schaden
nicht, sie werden nur nicht mehr gebraucht, und sie verschwinden dann mit dem
nächsten Umbau des Templates statt in derselben Nacht.
Fazit
Das Muster ist dasselbe wie bei der Fensterbreite als Zahl: Sobald eine Information als reine Zahl vorliegt, lässt sich jede Einheit daraus ableiten – Millisekunden, Grad, Prozent, Stapelebenen. Der Unterschied ist, dass die Position eines Elements schon immer im Dokument stand. Sie war nur nie abrufbar, und deshalb hat man sie ins Markup kopiert oder nachträglich mit JavaScript eingesammelt.
Ich würde deswegen nichts umbauen, was heute funktioniert. Aber jede Liste, deren Länge jemand anders bestimmt, ist ein Kandidat – und das sind in redaktionell gepflegten Seiten fast alle.
Quellen
Syntax, Rückgabetypen und der Stand der Browser-Unterstützung nach den MDN-Referenzen zu sibling-index() und sibling-count().