CSS: cómo cambiar los estilos de un conjunto de elementos cuando haya más de un número determinado de ellos

02/11/2018
Artículo original

Imagina que en una aplicación web tienes un listado de fichas de producto en el que cada elemento es una pequeña estructura HTML para mostrar toda la info de cada uno de los productos del listado: foto, nombre, descripción, tamaños, colores, precio... Además, ese conjunto de elementos de información se generan desde una aplicación en el lado servidor, en función de alguna búsqueda o filtro de la base de datos, de modo que no puedes saber de antemano desde tu HTML cuántos elementos hay en la lista.

Dependiendo de cuántos haya, quieres cambiar el diseño del listado de resultados. Por ejemplo, si hay muy pocos (3 o menos) quieres que se vean más grandes, con la foto más prominente, más anchos y resaltándolos todo lo que puedas. Sin embargo si hay más de 3 (4 o más) la foto será más pequeña, ocultarás la descripción y en general la ficha de cada producto en el listado será menos llamativa.

Para hacer esto, cabría pensar que necesitarías algún tipo de colaboración por parte del servidor: enviarte el número de elementos en una variable de JavaScript o cambiar la estructura del HTML de cada producto o los estilos antes de enviar la página al navegador. Por supuesto, todo esto y otras cosas se pueden hacer pero ¿no sería mucho más interesante hacerlo sin necesidad de programar, usando tan solo CSS?.

Esto es precisamente lo que vamos a hacer.

Para lograrlo podríamos pensar en utilizar algún tipo de contador CSS o algo similar. Sin embargo podemos conseguirlo de una manera mucho más sencilla sacando partido a un par de pseudo-clases: una muy común y otra menos utilizada.

Primero, vamos a ver el ejemplo que voy a utilizar de muestra. No me voy a complicar la vida maquetando un listado de productos (lo mío no es el diseño, sino la programación), así que simplemente voy a meter una sencilla lista de elementos dentro de una caja, pero todo lo que voy a explicar serviría igual cambiando los selectores por los apropiados para tu diseño. Lo que importa es el concepto.

Así, si tengo este código HTML:

<div id="caja">
<ul id="lista">
<li>Elemento 01</li>
....
<li>Elemento N</li>
</ul>
</div>

y esta simple regla CSS:

#caja {
border: 1px solid black;
width: 500px;
height: 75px;
overflow: hidden;
}

que hace que la caja contenedora tenga un cierto ancho pero sea mucho más baja, con una relación de aspecto grande, lo que veré por pantalla al renderizar la página será esto:

Es decir, a partir de 3 elementos ya no se verá nada porque la caja los tapará. Lo que quiero conseguir en este ejercicio es que, cuando la lista tenga 4 o más elementos (o sea, más de 3) el estilo de éstos cambie y en lugar de mostrarlos con el aspecto por defecto (con la bolita a la izquierda y unos debajo de otros), los muestre unos al lado de los otros (en línea) y con un separador entre ellos. Algo así:

De modo que aprovechen el espacio horizontal y puedan caber todos.

Vale, este es un ejemplo feo, pero sirve para ver la idea y ponerla en práctica sin complicarnos con estructuras de HTML muy complicadas.

Vamos a ver cómo hacerlo...

En primer lugar necesitamos ver cómo podemos determinar que hay más de "x" elementos en la lista. Para ello podemos intentar lo más evidente, que es usar la pseudo-clase :nth-child que sirve para seleccionar los elementos hijo de otro, usando para ello una fórmula. Por ejemplo:

#lista li:nth-child(4) {
color: red;
}

selecciona el cuarto elemento de tipo li que forma parte de la lista #lista, y le pone un color rojo a su texto. Si quisiésemos que seleccionase no solo el cuarto, sino también todos los siguientes, podríamos escribir:

#lista li:nth-child(n+4) {
color: red;
}

en este caso la variable n se sustituye los valores 0, 1, 2... y así sucesivamente y se aplica la fórmula para determinar qué elementos serán seleccionados (la fórmula general es an+b, siendo a y b constantes que nosotros ponemos y n la variable que se sustituye por parte de CSS). De este modo, todos los elementos del cuarto (incluído) en adelante tendrán un aspecto diferente, dejando los tres primeros con el aspecto por defecto:

(he quitado la altura a la caja para que se puedan ver todos y ver que han cambiado)

Esto estaría bien si siempre quisiésemos que los 3 primeros tuvieran un determinado aspecto y los siguientes otro diferente. Pero no es lo que buscamos. Lo que necesitamos es que cambien TODOS el aspecto cuando haya más de un número determinado, no solo los que sobrepasen dicho número.

Para lograrlo vamos a seguir una estrategia parecida pero inversa, que aprovechará un combinador de selectores especial para alcanzar nuestro objetivo.

Existe otra pseudo-clase complementaria de la anterior, :nth-last-child, que se comporta igual pero empieza a contar los elementos desde el final, o sea, al revés. Si ponemos:

#lista li:nth-last-child(n+4) {
color: red;
}

Lo que conseguiremos es seleccionar todos los elementos a partir del tercero desde el final, así:

Es decir, justo lo contrario que lo anterior. No parece que hayamos ganado demasiado... Aguanta un poco conmigo y verás porqué esto, a pesar de parecerse, nos va a ayudar a conseguir lo que buscamos...

Otro de las pseudo-clases que tenemos disponibles en CSS es :first-child que nos permite seleccionar el primer elemento hijo de otro. Así, si por ejemplo escribo:

#lista li:first-child {
color: red;
}

seleccionaré el primer elemento:

Una cosa que no es muy común ver por ahí pero que se puede hacer es combinar dos pseudo-clases para forzar que se cumplan a la vez.

¿Qué ocurre si combinamos las dos pseudo-clases que acabamos de ver?, es decir, esto:

#lista li:first-child:nth-last-child(n+4) {
color: red;
}

pues que vamos a seleccionar el primero de los elementos de lista dentro de la lista #lista que además forme parte de los que están desde el 3º del final hasta el principio. El resultado es exactamente el mismo que el anterior, o sea, esto:

ya que se selecciona el primer elemento, pero la diferencia estriba en que ahora solo se seleccionará el primer elemento si hay al menos 4 (o sea, 3 o más), ya que se deben cumplir las dos condiciones. Si por ejemplo eliminamos de la lista todos los elementos menos los 2 o 3 primeros, el selector no seleccionará al primer elemento, que no se verá rojo (puedes probarlo). ¡Genial!

De todos modos aún no tenemos la solución ya que de momento solo hemos seleccionado el primer elemento, eso sí, cuando se cumpla la condición que queríamos. ¿Cómo hacemos que además se seleccionen todos los elementos en este caso?

Muy sencillo: haciendo uso del combinador general de hermanos, denotado por el símbolo ~ (la virgulilla o tilde). Lo que hace este operador cuando lo metemos en un selector es que selecciona todos los hermanos del elemento actual que estén a continuación en el DOM. Es decir, en el mismo nivel y después del actual.

Dado que con las pseudo-clases anteriores tenemos seleccionado el primer elemento solo cuando haya más de "x" (3 en nuestro ejemplo), basta con añadirle este combinador y elegir todos los elementos posteriores con él, así:

#lista li:first-child:nth-last-child(n+4) ~ li {
color: red;
}

que selecciona todos los elementos de la lista a continuación del primero solo cuando haya 4 o más, o sea, veremos esto:

Con esto CASI tenemos lo que queríamos. En realidad así seleccionamos todos menos el primero de la lista. Para incluirlo, lo único que tenemos que hacer es combinar los dos últimos selectores que hemos visto usando la coma para que actúen los mismos estilos en ambos casos: el que selecciona el primero cuando hay más de "x" y el que selecciona a los restantes, así:

#lista li:first-child:nth-last-child(n+4),
#lista li:first-child:nth-last-child(n+4) ~ li {
color: red;
}

¡Listo! Así tenemos a todos seleccionados.

Si en vez de cambiarle el color a rojo le cambiamos el estilo de visualización para que se muestren en línea (restaura también la altura de la caja), tendríamos esto:

#lista li:first-child:nth-last-child(n+4),
#lista li:first-child:nth-last-child(n+4) ~ li {
display: inline;
}

y veríamos esto cuando haya más de 3 elementos:

Lo único que nos falta es incluir un separador entre ellos, como decíamos al principio que íbamos a necesitar. Esto es muy sencillo y directo usando el pseudo-elemento ::before, así:

#lista li:first-child:nth-last-child(n+4) ~ li:before {
content: " | ";
}

que lo que hace es meter ese carácter de barra vertical (pipe) como separador en todos los elementos menos en el primero (mira la lógica de esto más arriba, pues es la misma que expliqué cuando conseguíamos que se seleccionaran todos menos el primero cuando hay más de "x".

Usando estas dos últimas reglas a la vez, conseguimos exactamente el efecto que buscábamos: solo se cambiará el aspecto cuando haya más de un número determinado de elementos en la lista.

Esto lo puedes extrapolar muy fácilmente a cualquier diseño HTML que se repita dentro de una página, aunque sea complejo. Si además usas SASS puedes crear muchas reglas basadas en esto sin tener que repetir este selector tan largo todo el rato.

Te dejo mi ejemplo aquí (ZIP, 640 bytes) para que juegues con él.

Esta técnica me encanta porque es un ejemplo más que demuestra que, a veces, CSS puede llegar a ser muy potente y otorgar algo de lógica a una estructura con un lenguaje muy limitado.

¡Espero que te resulte útil!