Volver a las notas

CSS ya sabe contar: sibling-index() y sibling-count()

18 de octubre de 2026·
csslayoutanimationbaseline
Demo en vivoAbrir a pantalla completa

CSS ya puede decirle a un elemento qué posición ocupa entre sus hermanos y cuántos hermanos hay, como números normales que puedes usar en calc(). sibling-index() y sibling-count() son Baseline Newly available desde el 18 de agosto de 2026, cuando las incluyó Firefox 154.

Parece poca cosa hasta que recuerdas los apaños. Una entrada escalonada suponía una regla :nth-child() por elemento, o un JavaScript que escribía style="--i: 3" en cada uno y lo reescribía cada vez que cambiaba la lista. Un menú circular suponía calcular ángulos en un script. Ahora lo hace la hoja de estilos, y sigue siendo correcto cuando añades o quitas elementos.

Qué devuelven

Según la especificación CSS Values and Units Level 5:

  • sibling-index() es la posición del elemento entre sus hermanos, como <integer>. Igual que :nth-child(), empieza en 1.
  • sibling-count() es el número total de elementos hijos del padre, incluido el propio elemento.

Ninguna recibe argumentos. Solo cuentan elementos, así que los nodos de texto y los comentarios entre ellos no cambian los números. MDN también señala la diferencia con counter(): un contador te da una cadena que solo puedes pintar con content, mientras que estas devuelven enteros con los que puedes hacer cuentas.

La versión mínima

Este es el escalonado de toda la vida. Cada elemento espera un poco más que el anterior:

li {
  animation: fade-in 0.4s backwards;
  animation-delay: calc(sibling-index() * 60ms);
}

@keyframes fade-in {
  from { opacity: 0; translate: 0 8px; }
}

No hace falta una clase por elemento ni un script. Si insertas un elemento en medio, todos los que van detrás suben una posición solos.

sibling-count() entra en juego cuando la disposición depende del total. Para repartir elementos en un círculo, cada uno se lleva su parte de los 360 grados:

li {
  --a: calc(360deg / sibling-count() * (sibling-index() - 1));
  transform: rotate(var(--a)) translateY(-128px) rotate(calc(-1 * var(--a)));
}

El primer rotate() orienta el elemento hacia su hueco, translateY() lo empuja hasta el radio y el segundo rotate() lo endereza para que la etiqueta quede derecha. Es la disposición «Anillo» de la demo de arriba. Si quitas una bola, las demás reciben un índice y un ángulo nuevos, y la transition sobre transform las lleva a su sitio con animación.

Las otras dos disposiciones usan la misma idea. «Abanico» centra los índices en cero con var(--i) - (var(--n) + 1) / 2 y los multiplica por un ángulo pequeño. «Girasol» gira cada elemento el ángulo áureo (137,5°) multiplicado por su índice y lo aleja sqrt(índice / total), lo que da esa espiral de pipas. El panel de debajo de la demo enseña el CSS de la disposición activa.

La trampa: propiedades personalizadas e hijos

Escribir --i: sibling-index() es cómodo, pero una propiedad personalizada normal no guarda el número. Guarda los tokens sibling-index(), y estos se evalúan allí donde uses var(--i). Si un hijo del <li> lee var(--i), obtiene su propio índice, no el del <li>. Lo comprobé en Chromium: un <span> que era el segundo hijo dentro del primer <li> obtenía 2, no 1.

La solución es registrar la propiedad como entero. Así se resuelve en el elemento donde la declaras y los hijos heredan el número:

@property --i { syntax: "<integer>"; inherits: true; initial-value: 1; }
@property --n { syntax: "<integer>"; inherits: true; initial-value: 1; }

li { --i: sibling-index(); --n: sibling-count(); }
li > button { counter-reset: idx var(--i); }
li > button::before { content: counter(idx); }

Así pinta la demo el número de cada bola: el entero se convierte en contador y el contador en texto. Con los pseudoelementos hay menos de qué preocuparse: según la especificación, en ::before y compañía estas funciones se resuelven igualmente contra el elemento de origen.

Soporte en navegadores y plan B

Según los datos de compatibilidad de MDN, el soporte empieza en Chrome y Edge 138, Safari 26.2 y Firefox 154. Es reciente, así que todavía tiene sentido un plan B durante un tiempo.

La detección cabe en una línea, y el método antiguo de escribir los números desde JavaScript sirve de alternativa:

if (!CSS.supports('width', 'calc(1px * sibling-index())')) {
  const items = list.children;
  for (let k = 0; k < items.length; k++) {
    items[k].style.setProperty('--i', k + 1);
    items[k].style.setProperty('--n', items.length);
  }
}

La demo hace exactamente eso y muestra un aviso cuando le toca. Como el resto del CSS solo lee var(--i) y var(--n), no hay que cambiar nada más. Los estilos en línea ganan a la declaración con sibling-index() de la hoja de estilos, así que si están los números de JavaScript, mandan ellos.

Dos cosas más de la especificación que conviene saber:

  • Estas funciones trabajan sobre el árbol DOM, no sobre el árbol aplanado, igual que :nth-child(). El contenido asignado a un slot se cuenta según su posición en el light DOM.
  • Si una regla intenta contar elementos de un shadow tree desde fuera, por ejemplo con ::part(), la función devuelve 0. Es intencionado, para que no se filtre la estructura del shadow DOM.

Qué vigilar

  • Soporte más amplio. Baseline Newly available significa que las versiones actuales de todos los navegadores principales las soportan. Quien use versiones antiguas seguirá necesitando el plan B hasta que la función sea Widely available, 30 meses después de la fecha en que pasó a Newly available.
  • Menos escaleras de :nth-child(). Las listas largas de reglas :nth-child(1), :nth-child(2)… de los sistemas de diseño son lo primero que se puede sustituir.
  • Junto a @property. Si vas a reutilizar el índice en elementos hijos, registra la propiedad personalizada desde el principio. Te ahorras el fallo más confuso que pueden darte estas funciones.

Fuentes