Zehn Regeln für zehn Elemente
sind neun zu viel.

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:

resources/css/liste.css
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.

resources/css/leistungen.css
.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.

Verzögerung und Farbe je Position calc((sibling-index() - 1) * 120ms)
Das erste Element liegt oben calc(sibling-count() - sibling-index())
Neun identische, leere Elemente – im Markup unterscheidet sie nichts, kein Attribut, keine Klasse. Den Unterschied macht allein das Stylesheet. Im Stapel darunter liegt die 1 oben statt der 5; in einem Browser ohne die beiden Funktionen laufen die Balken im Gleichtakt und die 5 liegt oben.

Farbe aus der Position

Aus beiden Zahlen zusammen wird ein Anteil, und ein Anteil ist ein Prozentwert – damit steuert die Position direkt eine Farbmischung:

resources/css/legende.css
.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:

resources/css/avatare.css
.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.

resources/css/leistungen.css
.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().

Fragen zum Thema – oder ein Projekt, das genau daran hängt?

Schreib mir über das Kontaktformular kurz, worum es geht. Ich schaue mir deinen Fall an und sage dir konkret, was sich verbessern lässt – unverbindlich und ohne Verkaufsgespräch.