La anatomía de An+B: rangos, ventanas y consultas de cantidad
Cómo evalúa el motor la microsintaxis An+B, cómo se construyen rangos intersecando dos condiciones, y por qué nth-child no sigue el orden visual.
Casi todo el mundo usa :nth-child() con dos valores: odd y even. La microsintaxis An+B es un lenguaje pequeño pero completo para describir subconjuntos de hermanos, y su verdadera potencia no está en las progresiones aritméticas sino en la intersección: encadenar dos pseudo-clases para recortar una ventana. Con eso, y con la variante que cuenta desde el final, se resuelven sin JavaScript los layouts que dependen de cuántos elementos hay.
- Evaluar mentalmente cualquier expresión
An+Bsin equivocarte de signo. - Construir rangos cerrados intersecando dos pseudo-clases estructurales.
- Escribir consultas de cantidad con
:nth-last-child()y el combinador de hermanos. - Predecir cuándo el orden que cuenta el selector no coincide con el orden que ve el usuario.
Cómo se evalúa An+B
La expresión An+B no describe un elemento: describe un conjunto de posiciones. El motor sustituye n por 0, 1, 2, 3… y se queda con los resultados que sean enteros positivos. Nada más.
Para 2n+1 la serie es 1, 3, 5, 7… que es lo mismo que odd. Para 3n es 0, 3, 6, 9… y como el 0 no es una posición válida, empieza en el tercero. Para 3n+1 es 1, 4, 7, 10.
El caso que confunde es A negativo, porque la serie decrece y se corta sola. En -n+3 la sustitución da 3, 2, 1, 0, -1… y al descartar lo que no es positivo queda {3, 2, 1}: los tres primeros. Ese es el idioma para “los N primeros”, y no hay otro.
| Expresión | Serie | Significado |
|---|---|---|
2n |
2, 4, 6… | pares, igual que even |
2n+1 |
1, 3, 5… | impares, igual que odd |
3n+2 |
2, 5, 8… | uno de cada tres empezando por el segundo |
n+4 |
4, 5, 6… | del cuarto en adelante |
-n+3 |
3, 2, 1 | los tres primeros |
n |
1, 2, 3… | todos |
0n+5 |
5 | solo el quinto, igual que 5 |
La gramática admite espacios alrededor del signo que separa An de B, pero no dentro de An. :nth-child(2n + 1) es válido; :nth-child(2 n+1) no lo es. Y + delante es opcional: +2n+1 y 2n+1 son la misma cosa.
Rangos y ventanas
Aquí está lo que casi nadie usa. Las pseudo-clases se pueden encadenar sobre el mismo sujeto, y encadenar significa intersecar.
/* Del tercero al septimo, ambos incluidos. */
li:nth-child(n+3):nth-child(-n+7) { font-weight: 600; }
Se lee como dos condiciones que se cumplen a la vez: “está en la posición 3 o posterior” y “está en la posición 7 o anterior”. La intersección es el rango cerrado. Con eso tienes todos los rangos imaginables sin escribir un solo número mágico repetido:
/* Todo menos los dos primeros. */
li:nth-child(n+3) { }
/* Solo los cuatro ultimos. */
li:nth-last-child(-n+4) { }
/* Ni los dos primeros ni los dos ultimos. */
li:nth-child(n+3):nth-last-child(n+3) { }
/* Los impares dentro del rango 5..12. */
li:nth-child(odd):nth-child(n+5):nth-child(-n+12) { }
La última línea muestra que la intersección se compone sin límite y sin paréntesis extra. Es álgebra de conjuntos escrita en selectores.
Cuando te encuentres un -n+K y dudes, sustituye mentalmente n por 0: obtienes K, el valor más alto de la serie. A partir de ahí la serie solo baja. Por tanto -n+K significa siempre “los K primeros”. Y para n+K, sustituye n por 0 y obtienes K, el valor más bajo: “del K en adelante”. Con esos dos anclajes no vuelves a equivocarte de dirección.
Contar desde el final
:nth-last-child() es idéntico a :nth-child() salvo que cuenta desde el último hermano hacia atrás. Su utilidad no es cosmética: es lo que permite preguntar por el total sin conocerlo.
El razonamiento es indirecto y merece verlo despacio. Si un elemento cumple :nth-last-child(n+5), hay al menos cuatro hermanos después de él. Si además es el primero, :first-child:nth-last-child(n+5), entonces la lista entera tiene al menos cinco elementos. Y como el primero no puede estilar a los demás, se propaga con el combinador de hermanos:
/* Si hay cinco o mas elementos, todos se ponen en dos columnas. */
li:first-child:nth-last-child(n+5),
li:first-child:nth-last-child(n+5) ~ li {
--ancho: 50%;
}
/* Exactamente tres elementos. */
li:first-child:nth-last-child(3),
li:first-child:nth-last-child(3) ~ li {
--ancho: 33.333%;
}
Este es el patrón histórico de las consultas de cantidad, y sigue siendo la forma correcta cuando el sujeto son los hijos. Cuando el sujeto es el contenedor, :has() lo escribe mucho mejor, como viste en el catálogo de patrones: .lista:has(> :nth-child(5)) dice lo mismo en una línea y sin duplicar el selector. Las dos herramientas coexisten porque responden a preguntas distintas: una estila los hijos, la otra estila el padre.
Un detalle que ahorra depuraciones: :only-child equivale exactamente a :first-child:last-child, y :last-child equivale a :nth-last-child(1). No son atajos aproximados, son sinónimos con la misma especificidad de pseudo-clase.
Lo que nth-child no cuenta
Tres fuentes de sorpresa, en orden de frecuencia.
No cuenta nodos de texto ni comentarios. Solo elementos. El salto de línea y la indentación entre etiquetas no ocupan posición. Esto es bueno y es lo que esperas.
Sí cuenta los elementos ocultos. Un hermano con display: none sigue ocupando su posición en el conteo, porque la posición es una propiedad del árbol del DOM y display es una propiedad de la caja, que se decide después. Si filtras una lista ocultando elementos, tus rayas de cebra se descuadran. La solución no es cambiar el selector: es quitar los elementos del DOM, o usar of S, que verás en la siguiente lección.
No sigue el orden visual. Esta es la que más duele. :nth-child() describe la posición en el árbol; el orden en que se pintan las cajas puede ser otro completamente distinto. La propiedad order de flexbox reordena la presentación sin tocar el DOM. grid-auto-flow: dense recoloca elementos para tapar huecos. flex-direction: row-reverse invierte la fila. En los tres casos, :nth-child(1) sigue apuntando al primero del HTML, que ya no es el que está a la izquierda.
.barra { display: flex; }
.barra .logo { order: -1; }
/* Esto NO estila el elemento que se ve primero. Estila el primero del HTML. */
.barra > :nth-child(1) { margin-inline-end: auto; }
Es tentador leer :nth-child(1) como “el primero” y dar por hecho que coincide con lo que el usuario ve arriba a la izquierda. La separación es deliberada y va mucho más allá de una molestia con order. CSS mantiene tres órdenes distintos que solo coinciden por accidente: el orden del documento, que es el del HTML y el que ven los selectores estructurales y la tecnología de asistencia; el orden visual, que resulta del algoritmo de layout y que puede reordenarse, invertirse o compactarse; y el orden de navegación por teclado, que en la plataforma moderna sigue al orden del documento y no al visual. Cuando usas order o dense estás desincronizando los tres a la vez, y ahí es donde nace ese bug de accesibilidad que ninguna herramienta automática detecta: el usuario ve los botones en un orden y el tabulador los recorre en otro. Esto convierte una decisión aparentemente estética en una decisión de accesibilidad, y da una regla operativa que merece la pena adoptar sin excepciones: si te apoyas en selectores estructurales, no reordenes visualmente; y si necesitas reordenar visualmente, hazlo en el DOM. Los casos donde reordenar con CSS es legítimo son aquellos en los que el orden visual no transmite información —una rejilla decorativa, un mosaico— y en cuanto el orden significa algo, moverlo desde CSS es mentirle a media plataforma.
- Escribe el selector para “del quinto al décimo, solo los pares”.
- Escribe el selector para “todos salvo el primero y el último”.
- Construye una consulta de cantidad que aplique tres columnas cuando haya entre seis y nueve elementos.
- Demuestra con un ejemplo que un hermano con
display: nonesigue contando. - Monta una barra con
ordery comprueba con el tabulador que el foco no sigue el orden visual.