Diseño matemático usando sibling-index() y sibling-count() — Smashing Magazine
sibling-index() y sibling-count(). Una línea de CSS logra un efecto de cascada escalonada sin :nth-child() Reglas o solución JS. Válido para 5 artículos o 5.000 unidades.
¿Sabes eso: tienes una fila de cartas y quieres que desaparezcan una por una? Este efecto de cadena escalonada. Se ve genial. Debería ser sencillo. Sin embargo, cada vez que lo construyo, su implementación me hace sentir como si estuviera haciendo algo fundamentalmente estúpido.
Ver Pen (animación dinámica escalonada usando CSSsibling-index() (bifurcado)) (https://codepen.io/smashingmag/pen/zxowBog) Durgash.
Porque las opciones son siempre las mismas. Suponga que desea implementar un retraso de animación escalonado en una lista de 10 elementos. O escribes un bucle Sass que genera una docena :nth-child() Reglas, cada una codificada --index Variables en esa ubicación específica:
/* One rule per item. Hope the list never grows. */
li:nth-child(1) { --idx: 1; }
li:nth-child(2) { --idx: 2; }
li:nth-child(3) { --idx: 3; }
/* ... eight more of these ... */
li:nth-child(10) { --idx: 10; }
li {
animation-delay: calc(var(--idx) * 100ms);
}
Diez artículos. Diez reglas. ¿Qué pasa si la lista crece a 50 personas? Lo limitas y esperas lo mejor, o configuras un bucle Sass que genera cientos de selectores en el momento de la compilación. A ingenieros como Roman Komarov se les ocurrió Estrategia O(×N) – Cosas bastante inteligentes, pero aún así terminas con 63 reglas que cubren 1.023 elementos.
O recorre elementos en JavaScript y les aplica estilo en línea. style="--index: 3". Justo en el DOM. Funciona bien. También propaga problemas de diseño en sus scripts y se interrumpe silenciosamente seis meses después, cuando alguien refactoriza el elemento sin darse cuenta de que CSS se basa en variables inyectadas con JavaScript.
Ambos métodos siempre me molestan por las mismas razones: Le dices al navegador algo que ya sabe.. Navegador construido Árbol DOM. Sabe qué elemento es el tercer hijo. Tiene datos. CSS simplemente no tiene acceso a él.
Bueno, ahora lo hace:
li {
animation-delay: calc(sibling-index() * 100ms);
}
Una línea. Válido para 5 artículos o 5.000 unidades. No hay oyentes de eventos. No hay observadores de mutaciones. Sin volver a renderizar.
sibling-index() y sibling-count() es parte de Valores y Unidades CSS Mod Nivel 5 Especificación (Sección 9, si eres el tipo de persona que lee los borradores del W3C por diversión). La propuesta es Aprobado a través del número 4559 de CSSWG Después de una discusión sustancial. Las funciones en sí no toman ningún parámetro, simplemente los usas.
sibling-index()Da la posición basada en 1 de un elemento entre los hijos de su padre. El primer niño ha vuelto.1. El quinto hijo ha vuelto.5. Solo cuenta los nodos de elementos: los nodos de texto, las anotaciones y los espacios en blanco son invisibles para él.sibling-count()Proporciona el número total de elementos secundarios que tiene el elemento principal. Básicamente, CSS es equivalente aelement.parentElement.children.lengthEn JavaScript, pero se puede utilizar en hojas de estilo.
Ambas funciones resuelven <integer> – No <string>un número real. Esto significa que puedes tirarlos calc(), min(), max(), round(), mod()funciones trigonométricas y similares sin() y cos(). cuando escribes calc(sibling-index() * 100ms)CSS maneja la coerción de tipos y genera resultados válidos. <time> valor. No se requieren habilidades. comparar counter()que devuelve una cadena y sólo puede existir internamente content Para los pseudoelementos, el asunto es completamente diferente.
Una aclaración confusa: :nth-child() es un selector. Selecciona elementos. No genera valor. no puedes escribir calc(:nth-child() * 10px) –Esto no es CSS válido. sibling-index() Lo contrario es cierto. Va en su declaración y le da un número que puede usar para calcular. Resuelven diferentes problemas y hasta ahora los hemos estado grabando. :nth-child() Asumir un papel para el que nunca fue diseñado.
Patrones que vale la pena robar
Una vez que descubres que estos son sólo números enteros, las ideas surgen rápidamente.
entrelazado inverso
¿Quieres que tu último proyecto sea animado primero? menos:
.card {
animation: fade-in 0.4s ease both;
animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}
el último niño recibe (N - N) * 80ms = 0ms – Se dispara inmediatamente. el primer niño recibe (N - 1) * 80ms. Las animaciones comienzan a reproducirse cuando se carga la página, en lugar de detenerse a un ritmo incómodo.
Ancho igual automático
Deje de contar niños manualmente para establecer un porcentaje:
.tab {
width: calc(100% / sibling-count());
}
¿Cinco etiquetas? 20% por persona. ¿Agregar un sexto? 16,66%. ¿Quitar dos? 25%. Sin consultas de medios, sin cambio de tamaño del observador, sin JavaScript en absoluto.
Dicho esto, puedes imaginar un escenario en el que demasiados elementos dan como resultado una pestaña muy apretada, momento en el cual quizás quieras usar otra cosa, tal vez una solución de envoltura Flexbox.
Distribución de tonos
Distribuya los colores uniformemente en la rueda de colores:
.swatch {
background-color: hsl(
calc((360deg / sibling-count()) * sibling-index()) 70% 50%
);
}
Los tonos de los tres proyectos difieren en 120°. Los 12 elementos están en incrementos de 30°. La paleta se adapta a lo que sea que esté en el DOM, que es lo que suelen hacer las bibliotecas de colores de JavaScript.
En JavaScript, distribuir elementos en un círculo significa calcular el seno y el coseno. CSS ahora tiene sin() y cos() lugar nativo (Juan Diego Rodríguez tiene una gran Tutorial práctico Estos están en CSS-Tricks), y combinado con el conteo de árboles, todo se convierte en CSS puro:
.radial-item {
--angle: calc((360deg / sibling-count()) * sibling-index());
--radius: 120px;
position: absolute;
left: calc(50% + var(--radius) * cos(var(--angle)));
top: calc(50% + var(--radius) * sin(var(--angle)));
transform: rotate(calc(var(--angle) * -1));
}
¿Seis artículos? hexágono. ¿ocho? Forma octogonal. Agregue o elimine elementos y se volverá a calcular el diseño. No hay JavaScript para calcular coordenadas.
Apilamiento de índice Z
¿Crear un abanico de cartas? Una línea:
.card {
z-index: calc(sibling-count() - sibling-index());
}
La primera carta de la pila es la más alta y la última carta es el 0. Si quieres lo contrario, invierte las matemáticas.
trampa
Estos merecen una discusión aparte ya que no son obvios en la especificación.
Alcance del DOM en la sombra
sibling-index() y sibling-count() Opere en el árbol DOM, no en el árbol visual aplanado. Esta diferencia definitivamente te morderá. componentes de red.
Digamos que tienes un elemento personalizado con este DOM oculto:
<section>
<slot></slot>
<div class="internal"></div>
</section>
si tu estilo .internal y sibling-index()que regresa 2. siempre. a pesar de <slot> Proyecto 300 elementos. La función ve a dos niños. <section> En el árbol de la sombra—— <slot> y .internal En lo que respecta al recuento de particiones, el contenido DOM ligero proyectado no existe.
También hay preocupaciones de seguridad aquí. Si una hoja de estilo DOM ligera intenta acceder a un componente a través de ::part() y usar sibling-index()el navegador regresa 0. Cero plano. Es una pared intencional colocada para evitar que CSS externo sondee la estructura interna de componentes de terceros. Sinceramente, creo que fue la decisión correcta.
Los pseudoelementos no cuentan
::before y ::after No hermanos. no aparecieron sibling-count() no tienen el suyo propio sibling-index(). Pero esta parte le ahorra tiempo de depuración. capaz Utilice estas funciones en declaraciones de pseudoelementos. cuando escribes #target::before { width: calc(sibling-index() * 10px); }que evalúa sibling-index() oponerse a #targetno para pseudoelementos. Los pseudoelementos no son nodos reales, por lo que la función vuelve a sus elementos originales. la misma historia con ::slotted(*)::before — Comprueba el índice del elemento ranurado en el DOM claro.
display: none sigue siendo importante
Esta cosa me quemó. elementos con display: none Desaparece del árbol de diseño. No ocupan espacio. No son visibles para los lectores de pantalla. Pero todavía están en el DOM.
desde sibling-index() Al leer el árbol DOM, no el árbol de diseño, se cuentan los elementos ocultos:
<ul>
<!-- sibling-index() = 1 -->
<li>Apple</li>
<!-- sibling-index() = 2, invisible -->
<li style="display:none">Banana</li>
<!-- sibling-index() = 3, NOT 2 -->
<li>Cherry</li>
</ul>
la cereza es 3No 2. Los plátanos escondidos todavía tienen su lugar.
Esto no importa para la mayoría de los diseños. Pero si estás creando algo como un filtro de búsqueda para ocultar elementos que no coinciden display: nonesu animación escalonada y su diseño circular crearán espacios. Los elementos visibles conservan su índice original no secuencial. Para cualquier cosa que dependa del conteo continuo (menús radiales, anchos proporcionales), es necesario eliminar los nodos filtrados del DOM, no solo ocultarlos. O recurrir a la indexación administrada por JavaScript.
notas: visibility: hidden y opacity: 0 Cuenta, pero se siente más intuitivo ya que estos elementos aún ocupan espacio. display: none es astuto porque el elemento desaparece visualmente pero aún ocupa una ranura DOM.
Las propiedades personalizadas se evalúan inmediatamente
Es muy sutil. Si intenta centrar el índice en el padre:
.parent {
--idx: sibling-index();
}
…eso --idx resuélvelo ahí .parent. Toma el índice hermano del padre, lo bloquea en ese número y cada hijo hereda ese valor fijo. Cada niño obtendrá el mismo número. Es casi seguro que no es lo que quieres o esperas.
La solución es simple: coloque la función en el elemento que la necesita:
.child {
--idx: sibling-index();
animation-delay: calc(var(--idx) * 100ms);
}
CSSWG discutió inherits: declaration Aparte de @property Este problema se puede resolver en teoría. Si aún no lo has usado @propertyque le permite definir el tipo de propiedad personalizada, el valor inicial y el comportamiento heredado: más control que las propiedades primitivas --variable. pero inherits: declaration Esta idea aún se encuentra en las primeras discusiones del CSSWG y aún no se ha incorporado en ningún borrador de especificación. Podría tardar años en aterrizar o puede que no aterrice en absoluto. incluso con @property Hoy en día no existe ningún mecanismo para decir “No juzgues ahora, espera al niño”. Así que presente su solicitud ahora.
Rendimiento a escala
Cambiar el DOM (es decir, agregar, eliminar, reordenar subelementos) activará un nuevo cálculo de los estilos en el mismo nivel afectado. Los navegadores manejan esto en la etapa de cascada (antes del diseño y la pintura), por lo que es más rápido que el antiguo método de bucle y etiquetado de estilos en línea en JavaScript.
Pero si se fuerza, hay costos reales. Insertar un elemento al comienzo de un contenedor de 10.000 elementos secundarios obliga al motor a recalcular los índices hermanos de todos los 10.000 elementos posteriores. Para las cosas comunes (navegación, cuadrícula de tarjetas, barra de pestañas), nunca lo notarás. Para obtener información bursátil en tiempo real o feeds de desplazamiento infinito con miles de nodos en constante movimiento, continúe usando índices administrados por JavaScript dentro de ventanas virtualizadas. Estas funciones son rápidas. No son de coste cero.
Soporte del navegador
Al momento de escribir este artículo, cromo/borde138 Estas funciones están disponibles en versiones estables (junio de 2025) y Safari 26.2 Seguido de cerca. Firefox aún no los ofrece en versiones estables, pero Mozilla La posición de la especificación es positiva. El trabajo de implementación está en marcha activamente (seguido a continuación) Problema de Bugzilla #1953973. Controlar Canio Obtenga las últimas novedades antes del envío.
Juntos, Chrome y Safari cubren entre el 75 y el 80 % del tráfico mundial. Esa es la gran mayoría, pero la ausencia de Firefox significa que todavía necesitas un recurso alternativo.
Envío hoy, @supports ¿Es tu amigo?
/* Baseline that works everywhere */
.item {
width: 25%;
animation-delay: 0ms;
}
/* Progressively enhance where supported */
@supports (z-index: sibling-index()) {
.item {
width: calc(100% / sibling-count());
animation-delay: calc(sibling-index() * 80ms);
}
}
Respaldo estático para Firefox. Diseño de matemáticas para otros. Nadie debería encontrarse jamás con una página rota.
Acerca de Polyfill:
El polyfill de JavaScript que recorre los hermanos y establece estilos en línea es lo que reemplazan estas funciones. Pero eso no significa que deba utilizar también valores alternativos codificados. Juan Diego Rodríguez escribe sobre “como esperar
sibling-count()ysibling-index()Función” establece el modelo correcto para la mejora progresiva hasta que el soporte nativo alcance una línea de base. Su enfoque utiliza técnicas CSS existentes (como la de Roman Komarov Trucos para contar) como puente en lugar de relleno completo de JavaScript. Vale la pena leerlo si necesita entregar un producto listo para producción hoy mientras Firefox se pone al día.
Instrucciones auxiliares de uso.
Es necesario decir esto porque es fácil emocionarse y olvidarse: Estas características son puramente visuales.. Cambian el aspecto de las cosas. no cambiarán nada medio.
si usas sibling-index() Listas de reordenamiento de matemáticas de forma intuitiva – a través de order o ubicación en la cuadrícula: los lectores de pantalla aún leen el DOM en el orden de origen. El orden de las pestañas del teclado también sigue el DOM. El diseño visual y la estructura semántica se contradecirán, lo que supone un fallo de accesibilidad.
Para componentes interactivos como cuadrículas de datos, menús radiales o cuadros de lista personalizados que dependen de recuentos de árboles para el diseño, aún necesita JavaScript para sincronizar las propiedades de ARIA. aria-posinset y aria-setsize No tengo idea de qué está calculando CSS. Si tu CSS dice “Visualmente este es el punto 3 de 7” Pero ARIA dice algo diferente (o no dice nada en absoluto) y la experiencia para los usuarios de tecnología de asistencia será terrible.
En cuanto a la depuración, la última versión de Chrome DevTools le permite inspeccionar los cálculos. sibling-index() y sibling-count() Muestre valores directamente en el panel Elementos, lo que ayuda cuando las matemáticas no funcionan como esperaba.

¿Qué está por pasar?
Las especificaciones actualmente solo son importantes todo Hermanos y hermanas elementales. Sin embargo, el CSSWG ha documentado la expansión del proyecto. Número 9572: uno of <selector> Argumento, coincide con lo que :nth-child() Ya apoyado.
algo como esto sibling-index(of .active) le permitirá contar solo los hermanos que coincidan con un selector específico. Un elemento que es el octavo hijo del total pero el tercero .active el niño volverá 3. Para la interfaz de usuario dinámica en la que desea filtrar o alternar la visibilidad, esto mantendrá el índice en orden sin necesidad de manipulación DOM.
El CSSWG también discutió este children-count() y descendant-count() Funciones: la primera le dirá cuántos hijos tiene un elemento (útil para diseños controlados por padres), la segunda contará recursivamente todos los descendientes. Ambos todavía están en la etapa de propuesta, pero ya han refinado la historia del conteo de árboles: sibling-index() y sibling-count() te ofrece una visión horizontal (¿dónde me ubico entre mis compañeros?), mientras children-count() y descendant-count() Te dará una vista vertical (¿qué hay debajo de mí?).
Ese sentimiento que mencioné en la parte superior – escribe diez :nth-child() Reglas para animaciones escalonadas, ¿te preguntas si te estás perdiendo algo obvio? Usted no. Lo obvio aún no ha llegado.
(gg, yk)