en CSS

Selectores avanzados CSS 2/4 – [attribute]

Después del artículo de selectores nth-child(), en este segundo capítulo sobre selectores avanzados vamos a ver uno de los selectores CSS más útiles, a la par que conocido, pero cuyo potencial, en mi opinión, no es aprovechado todo lo que se podría. Es el selector de atributos [attribute] que, si bien es cierto que no es tan común como el selector de clases o el selector de ID, seguro que os suena aunque aún no lo hayáis utilizado.

Selector CSS [attribute]

Sintaxis del selector [attribute]

La sintaxis básica es simplemente el nombre del atributo acotado por corchetes. Sirve para seleccionar todos los elementos que contengan dicho atributo, independientemente de su valor.

elemento {} /*especificidad 0,0,1 = especificidad 1*/
.clase {} /*especificidad 0,1,0 = especificidad 10*/
[atributo] {} /*especificidad 0,1,0 = especificidad 10*/
#identificador {} /*especificidad 1,0,0 = especificidad 100*/

Os preguntaréis a qué viene lo de la especificidad. Lo vemos un poco más adelante. De momento quedaos con los valores que tiene cada tipo de selector y vamos a ver el siguiente nivel de «especificidad» dentro del atributo. Se trata de determinar un valor para el atributo:

[atributo="valor"] {} /*las comillas son opcionales*/

De esta forma podemos seleccionar, por ejemplo, todos los enlaces externos que por lo general abriríamos en una ventana/pestaña nueva, usando [target=_blank]. Nada complicado, ¿verdad? A partir de aquí podemos jugar con una serie de opciones para afinar la selección, operadores lógicos que evalúan la coincidencia con el valor de un atributo cualquiera. Vamos a verlo!

Operadores del selector [attribute]

Los vamos a explicar usando el atributo class que creo es el que mejor nos permitirá entender las diferencias y a la vez nos servirá para continuar con los ejemplos en la última parte del artículo.

Operadores de valor del atributo

[atributo=valor] → [class=azulete]

El valor del atributo class es exactamente «azulete».

[atributo~=valor] → [class~=naranja]

El valor del atributo class contiene la palabra «naranja» entre la lista de valores separados por espacios. Esto es equivalante a declarar .naranja {}

[atributo|=valor] → [class|=cereza]

El valor del atributo class comienza por «cereza» dentro de una serie de valores separados por guiones. Este caso es útil para seleccionar, por ejemplo, elementos que contienen la etiqueta lang y comparten el mismo idioma pero en sus variantes territoriales:

<span lang="es-AR">Español de Argentina</span>
<span lang="es-CO">Español de Colombia</span>
<span lang="es-ES">Español de España</span>

Operadores de concordancia de sub-cadena

[atributo^=valor] → [class^=manzana]

El valor del atributo class comienza por la cadena (o prefijo) «manzana» independientemente de si está integrado dentro de otra palabra o cadena, de si va seguido por un guion o un espacio.

[atributo$=valor] → [class$=capital]

El valor del atributo class termina por la cadena «capital», sea o no parte de otra cadena de texto.

[atributo*=valor] → [class*=negrita]

El valor del atributo class contiene la cadena «negrita» en cualquier parte, al menos una vez.

Nota: Si el valor de la cadena está vacío, p. ej. [alt*=""], el selector no representa nada

Os dejo una pequeña batería de test para que los trasteéis y veáis como funcionan. Los nombres de las clases que he usado en las definiciones se corresponden, no con el código real de los ejemplos, sino el resultado visual para que os sea más fácil asociarlo.

See the Pen
CSS attribute selectors
by Daniel Abril (@elcssar)
on CodePen.

Clases CSS modulares

La razón para haberos explicado el funcionamiento de [attribute] usando clases como ejemplo tenía el único fin de llegar a esta parte del artículo. Aquí es donde viene la parte que El CSSar intenta aportar. Os voy a explicar una técnica en la que estuve trabajando ya hace unos años y que aplico cuando necesito mayor flexibilidad en mis frameworks.

[className] en lugar de .className

Hay varios motivos por los que usar selectores de atributo en lugar de selectores de clase estándar cobra sentido:

  1. En primer lugar te permite salvar la cuestión de las mayúsculas y minúsculas
  2. En segundo, [class] tiene la misma especificidad que .class, pero, lo que es más importante, se pueden combinar sus partes sin por ello añadir especificidad.
  3. Gracias a esa combinación salvamos problemas de especificidad derivados de la declaración de clases anidadas y también podemos evitar declaraciones agrupadas interminables.

Case-insensitive (insensible a mayúsculas/minúsculas)

Por defecto, en HTML4, los selectores son insensibles a mayúsculas y minúsculas, excepto para aquellos que, como el selector de atributo, dependen del idioma del documento. En HTML5, IDs, clases, nombres y valores de atributos vienen definidos por dicho idioma del documento. Sólo si por alguna razón se activa el Quirks mode, el ID y el atributo de clase se tratarán como ASCII case-insensitive.

Por lo tanto, ambos, selector de clase y de atributo, son case-sensitive por defecto. Sin embargo, el selector de atributo nos aporta una ventaja, pues puede forzarse que sea case-insensitive de forma muy simple.

[atributo="valor" i] /*La i convierte el selector en insensible a mayúsculas y minúsculas*/

Métodos de declaración de estilos y especificidad

Por lo general en CSS estamos acostumbrados a crear clases y, cuando necesitamos hacer excepciones para que en ciertos casos cambien algunas de sus reglas, anidar esas mismas clases bajo otros selectores que nos den mayor especificidad.

.clase1 {color: blue;} /*especificidad 0,1,0*/
.clasePadre .clase1 {color: red;} /*especificidad 0,1,0 + 0,1,0 = 0,2,0*/

Seguro que también os sonará crear clases para cada uno de las reglas CSS y combinar la clase base con la específica para diferenciar un caso muy específico. O incluso caer en la tentación de usar un ID.

.clase1 {color: blue;} /*especificidad 0,1,0*/
.clase1.rojo {color: red;} /*especificidad 0,1,0 + 0,1,0 = 0,2,0*/
.clasePadre .clase1.verde {color: green;} /*especificidad 0,1,0 + 0,1,0 + 0,1,0 = 0,3,0*/
#naranja {color: orange;} /*especificidad 1,0,0*/

Con esta forma de trabajar requieres de clases en múltiples niveles del HTML lo que genera dependencias que hacen que cuando quieres aplicar los mismos estilos a otras partes de la aplicación debas declarar nuevas dependencias para poder hacerlo, generando agrupaciones gigantescas.

.clasePadre .clase1, .clasePadre2 .clase1, .clasePadre3 .clase1, .clasePadre4 .clase1 {color: red;}

Sistemas como BEM intentan salvar el problema de especificidad agrupando niveles…

.clasePadre{ }
.clasePadre--simple { }
.clasePadre__input { }
.clasePadre__submit { }
.clasePadre__submit--disabled { }
/*todos tienen especificidad 0,1,0*/

Aunque siempre encontramos quienes usan BEM y siguen aplicándolo como lo hacían con nombres de clases normales:

.clasePadre .clasePadre__submit.clasePadre__submit--disabled { }

BEM tampoco es la solución porque esa forma de trabajar es todo lo contrario de modular, especialmente en manos inexpertas. Al asociar todas las clases de un componente a éste, las hace inservibles para otros componentes. Esto genera duplicidades y largas agrupaciones. Además, acabas añadiendo atributos class larguísimos y nada semánticos, cargando el DOM innecesariamente. Por no hablar de que el método en sí no soluciona la mala semántica.

Truco: ¿sabías que utilizando [id="tuIdentificador"] {...} en lugar de #tuIdentificador {...} reduces la especificidad de 100 (1,0,0) a 10 (0,1,0)?

Selectores modulares semánticos

Entonces ¿Cuál es la solución? Pues lo cierto es que la solución radica en dos puntos: seguir las buenas prácticas que nos instan a crear nombres de clases semánticos y, además, compaginarlo con la técnica que vamos a ver a continuación.

Ha quedado claro que…

  1. Tenemos tres niveles de especificidad: elementos (0,0,1) = 1, clases, atributos y pseudoclases (0,1,0) = 10, identificadores únicos o ID (1,0,0) = 100
  2. Por lo tanto será siempre preferible trabajar con la semántica de los elementos y cuando necesitemos mayor especificidad utilizar clases, evitando el uso de ID.
  3. Cuanta menos jerarquía menos especificidad, por lo que hay que evitar la anidación siempre que sea posible, pero sin condenarla, pues es parte de la naturaleza de los CSS.
  4. La semántica otorga la independencia necesaria para evitar el exceso de anidación y por tanto de especificidad, y dota de modularidad a nuestros componentes.
  5. El exceso de atomización de características o estilos también aumenta la especificidad pues debemos sumar varias clases para obtener el resultado final.
  6. El selector de atributo nos permite seleccionar partes de la cadena de su valor.

A partir de estas premisas nace la solución que consiste en crear clases y selectores modulares. Esto sería tal que así:

HTML
<section class="listaFlexResponsive">
<article class="producto">...</article>
<article class="productoOferta">...</article>
<article class="productoOfertaPrioritario">...</article>
</section>
CSS
[class^="producto"] {/*estilos generales para cualquier producto*/}
[class*="Oferta"] {/*estilos que lo identifican como una oferta*/}
[class*="Prioritario"] {/*estilos que lo identifican como un elemento prioritario, sea o no de tipo producto*/}

[class*="Prioritario" i] {/*estilos*/} /*si queremos dar independencia a la cadena de otras cadenas o clases y así poder aplicar la clase como class="prioritario"*/

Ventajas del método

  1. Ahorra agrupaciones
  2. Ahorra anidaciones y reduce la especificidad
  3. Otorga limpieza y legibilidad al código
  4. Favorece la modularidad

Ahorra agrupaciones

La técnica se basa en separar lo esencial de lo complementario y lo específico de lo común, por esa razón, un set de reglas comunes a varios módulos HTML, serán asociadas a un único selector que formará parte de múltiples clases compuestas declaradas en dicho HTML. De esta forma, evitaremos tener que agrupar esas múltiples clases asociadas a un set de reglas.

HTML
<ul class="listaProductos">...</ul>
<ul class="listaProductosOferta">...</ul>
<ul class="listaSocios">...</ul>
<ul class="listaClientes">...</ul>
CSS
/*Con el método tradicional de definición de clases*/
.listaProducto, .listaProductosOferta, .listaSocios, .listaSocios {/*reglas comunes*/}

/*Con el método modular con [attribute]*/
[class^="lista"] {/*reglas comunes para todas esas listas*/}

Ahorra anidaciones y reduce especificidad

La parte de las anidaciones es quizás más difícil de imaginar sin el ejemplo de un proyecto real porque normalmente se generan cuando nuestro proyecto comienza a hacerse más complejo. La clave del ahorro radica en la baja especificidad del tipo de selector. Aún siendo la misma que un .class, el hecho de concentrar las reglas en una única clase modular en lugar de repartir los átomos entre distintas clases, reduce los casos en los que necesitamos sumar especificidad para sobrescribir reglas.

Aunque el orden en que se definen las clases ya crea una jerarquía a la hora de sobrescribir las reglas, a veces, para poder mantener el código organizado, agrupamos los selectores por componentes que, a su vez, pueden estar agrupados de otras formas, lo que tiene todo el sentido del mundo, pero esto dificulta mantener esa jerarquía lineal. Esos diferentes componentes pueden compartir reglas, como sucede con los distintos tipos de listas y sus hijos (encabezados, listas anidadas, botones…). Como hemos visto, si nombramos a cada componente de forma muy específica, requeriría crear largas agrupaciones, para asociarlos a todos con las mismas reglas y, de ahí, surge la necesidad de atomización de dichas reglas.

Atomización CSS

Al atomizar separamos las reglas en selectores muy específicos pero esta vez NO asociados al componente, sino a la función o, a veces, a las características del elemento al que van asociadas. Esos elementos pueden ser de distintos tipos (ol, ul, li, div, etc.) y por ello tener definidas unas reglas generales para cada uno de ellos, y paralelamente compartir esos átomos que tendrán que sobrescribir algunas de esas reglas. La mayor especificidad de las clases ayuda a esa sobrescritura básica de reglas sin grandes problemas, sin embargo, cuando entran en juego jerarquías dentro del DOM por componentes embebidos (p. ej. listas dentro de listas) o ubicados en espacios diferentes (main, sidebar, footer…), nos encontramos con muchas herencias que habrá que sobrescribir. Ahí es donde el ahorro en especificidad es clave.

Tampoco hemos de olvidar que los átomos de esos componentes también pueden compartirse, como sucede con el ejemplo que hemos puesto de los «prioritarios» o, sin ser tan específico, un ítem de lista de «producto», por ejemplo (a veces los productos vendidos son tan dispares que su AI* es completamente diferente). Estos átomos compartidos tendrán las características comunes a todos los componentes, o al menos, a todos los componentes similares, pero pueden requerir algunas variaciones para algunos casos concretos dentro de la misma jerarquía del DOM que comparten con otros similares. En estos casos, con el método tradicional, no será suficiente con aplicar una clase específica porque su especificidad ya será superior a 0,1,0.

De esta forma cada vez que requerimos sobrescribir reglas, vamos añadiendo un poco más de especificidad al selector, convirtiéndose en una tendencia acumulativa. Llegando a un punto, donde la anidación es tan bestia, que nos vemos tentados a usar !important. Para que no sucumbáis a la tentación, y evitar que pequéis, os comparto esta solución!! 😉

*AI = Arquitectura de la Información

Como sería una locura montar todo el DOM que ejemplificase las distintas opciones, imaginaos los dos ejemplo anteriores combinados y repetidos por varios puntos del la estructura HTML:  una lista de enlaces en un <nav> del <header>, listas normales y ordenadas, y varias de distintos tipos (productos, clientes…), todas ellas dentro del <main>, del <sidebar> y del <footer>, etc…

El método tradicional de definición de clases CSS
ul, ol {/*reglas*/} /*0,0,1 y 0,0,1*/
li {/*reglas*/} /*0,0,1*/
ol li {/*reglas*/} /*0,0,2*/
.lista {/*reglas*/} /*0,1,0*/
.grid {/*reglas*/} /*0,1,0*/
/*Puesto que los items pueden ser parte de una lista o de un grid, será necesario combinarlos para diferenciarlos*/
.productos {/*reglas*/} /*0,1,0*/
.clientes {/*reglas*/} /*0,1,0*/
.lista.productos {/*reglas*/} /*0,2,0*/
.lista.clientes {/*reglas*/} /*0,2,0*/
.grid.productos {/*reglas*/} /*0,2,0*/
.grid.clientes {/*reglas*/} /*0,2,0*/
sidebar .lista {/*reglas*/} /*0,1,1 → Si el elemento tiene definido cualquier tipo de lista (productos, clientes...) esta regla ya no tendrá especificidad suficiente para sobrescribir las reglas genéricas de la listas del sidebar, tomando las genéricas que aplican en cualquier otra parte del DOM, por lo que si queremos sobrescribirlas deberemos ser aún más específicos sobrescribiendo cada uno de los tipos: */
sidebar .lista.productos {/*reglas*/} /*0,2,1*/
sidebar .lista.clientes {/*reglas*/} /*0,2,1*/
.lista li {/*reglas*/} /*0,1,1*/
.lista .producto {/*reglas*/}/*0,2,0*/
.lista.productos li {/*reglas*/} /*0,2,1*/
sidebar .lista .producto {/*reglas*/} /*0,2,1*/

Y podríamos seguir complicándolo con grid, con «ofertas», con «prioritario», etc…

CSS del método modular con [attribute]
ul, ol {/*reglas*/} /*0,0,1 y 0,0,1*/
li {/*reglas*/} /*0,0,1*/
ol li {/*reglas*/} /*0,0,2*/
[class^="lista"] {/*reglas*/} /*0,1,0*/
[class^="grid"] {/*reglas*/} /*0,1,0*/
[class*="productos" i] {/*reglas*/} /*0,1,0*/
[class*="clientes" i]{/*reglas*/} /*0,1,0*/
[class^="producto"] {/*reglas*/} /*0,1,0*/
[class*="Oferta" i] {/*reglas*/} /*0,1,0*/
[class*="Prioritario" i] {/*reglas*/} /*0,1,0*/

/*En el caso de diferenciar entre grid y list podremos hacerlo sin aumentar la especificidad*/

.listaProductos {/*reglas*/} /*0,1,0*/
.listaClientes {/*reglas*/} /*0,1,0*/

/*o también...*/
[class*="listaClientes" i] {/*reglas*/} /*0,1,0*/

De esta forma ser más específicos no implica añadir especificidad


/*Si requerimos indicar que es parte del sidebar, utilizar un selector de elemento, añadiendo un poco de especificidad, sigue siendo lo más recomendable para mantener el DOM limpio*/

sidebar .listaProductos {/*reglas*/} /*0,1,1*/
sidebar [class^="producto"]{/*reglas*/} /*0,1,1*/

Otorga limpieza y legibilidad al código

Como vemos en los ejemplos, al eliminar las agrupaciones y las anidaciones, la hoja de estilos queda mucho más limpia y fácil de leer. También su extensión se reduce, por lo que, en conjunto, estos beneficios facilitarán la depuración del código al ser más fácil de encontrar y analizar.

Favorece la modularidad

Declarar así los selectores, además, permite que el método de trabajo se adapte mejor al diseño atómico (atomic design) y llevarlo a un nivel aún inferior, a un nivel subatómico, pero siempre intentando conservar la semántica como base para crear y nombrar nuestras clases (átomos) y sus partes (neutrones y electrones) que en función de cómo se combinen nos darán átomos de un tipo o de otro, y de ahí la modularidad llevada al extremo.

Selector CSS [attribute] resumido

  • Permite seleccionar todo tipo de atributos (href, title, alt… incluso class e id)
  • Permite determinar si la coincidencia es case-sensitive o no, es decir tener en cuenta si está escrito en mayúsculas y minúsculas o considerarlas iguales
  • Tiene la misma especificidad que los selectores de clase y las pseudoclases
  • Además permite especificar con qué parte del valor queremos hacer coincidir el selector y si ésta es una palabra o una sub-cadena de texto.
  • Gracias a ello y utilizándolo concretamente con el atributo «class» y sus valores, podemos trabajar con clases modulares que nos ayudarán a mantener a raya la especificidad de nuestros selectores

Espero que con este segundo artículo sobre selectores avanzados os haya inspirado para utilizar más este fantástico selector [attribute] y, como siempre, espero vuestros comentarios.

En la próxima entrega «Selectores avanzados CSS 3/4 – :valid / :invalid» donde veremos esos y otros selectores avanzados que nos servirán para personalizar los estilos de validación de todo tipo de elementos de formulario. ¡No olvidéis suscribiros!

Escribe un comentario

Comentario