Domando la cascada con BEM y selectores CSS modernos PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.

Domar la cascada con BEM y selectores CSS modernos

BEM. Como aparentemente todas las técnicas en el mundo del desarrollo front-end, escribir CSS en formato BEM puede ser polarizante. Pero es, al menos en mi burbuja de Twitter, una de las metodologías CSS más populares.

Personalmente, creo que BEM es bueno y creo que debería usarlo. Pero también entiendo por qué es posible que no.

Independientemente de su opinión sobre BEM, ofrece varios beneficios, el mayor de los cuales es que ayuda a evitar conflictos de especificidad en CSS Cascade. Esto se debe a que, si se usa correctamente, cualquier selector escrito en formato BEM debe tener la misma puntuación de especificidad (0,1,0). He diseñado el CSS para muchos sitios web a gran escala a lo largo de los años (piense en el gobierno, las universidades y los bancos), y es en estos proyectos más grandes donde descubrí que BEM realmente brilla. Escribir CSS es mucho más divertido cuando tienes la seguridad de que los estilos que escribes o editas no afectan a ninguna otra parte del sitio.

En realidad, hay excepciones en las que se considera totalmente aceptable agregar especificidad. Por ejemplo: el :hover y :focus pseudoclases. Estos tienen una puntuación de especificidad de 0,2,0. Otro son los pseudo elementos, como ::before y ::after — que tienen una puntuación de especificidad de 0,1,1. Sin embargo, para el resto de este artículo, supongamos que no queremos ningún otro problema de especificidad. 🤓

Pero en realidad no estoy aquí para venderte BEM. En cambio, quiero hablar sobre cómo podemos usarlo junto con los selectores de CSS modernos: piense :is(), :has(), :where(), etc., para ganar incluso más, control de la cascada.

¿Qué es esto de los selectores CSS modernos?

El Especificaciones de nivel 4 de selectores CSS nos da algunas formas nuevas y poderosas de seleccionar elementos. Algunos de mis favoritos incluyen :is(), :where()y :not(), cada uno de los cuales es compatible con todos los navegadores modernos y es seguro de usar en casi cualquier proyecto hoy en día.

:is() y :where() son básicamente lo mismo excepto por cómo impactan la especificidad. Específicamente, :where() siempre tiene una puntuación de especificidad de 0,0,0. sí, incluso :where(button#widget.some-class) no tiene especificidad. Mientras tanto, la especificidad de :is() es el elemento en su lista de argumentos con la especificidad más alta. Entonces, ya tenemos una distinción en cascada entre dos selectores modernos con los que podemos trabajar.

El increíblemente poderoso :has() pseudo-clase relacional también es ganando rápidamente el soporte del navegador (y es la mayor característica nueva de CSS desde Cuadrícula, en mi humilde opinión). Sin embargo, en el momento de escribir este artículo, la compatibilidad del navegador con :has() todavía no es lo suficientemente bueno para su uso en producción.

Déjame pegar una de esas pseudoclases en mi BEM y...

/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
  /* styles for all somethings, except for the special somethings */
}

¡Vaya! ¿Ves esa puntuación de especificidad? Recuerde, con BEM lo ideal es que todos nuestros selectores tengan una puntuación de especificidad de 0,1,0. Por que es 0,2,0 ¿malo? Considere este mismo ejemplo, ampliado:

.something:not(.something--special) {
  color: red;
}
.something--special {
  color: blue;
}

Aunque el segundo selector es el último en el orden de origen, la mayor especificidad del primer selector (0,2,0) gana, y el color de .something--special los elementos se establecerán en red. Es decir, suponiendo que su BEM esté escrito correctamente y que el elemento seleccionado tenga tanto el .something clase base y .something--special clase de modificador aplicada a él en el HTML.

Usadas sin cuidado, estas pseudoclases pueden afectar a Cascade de formas inesperadas. Y son este tipo de inconsistencias las que pueden crear dolores de cabeza en el futuro, especialmente en bases de código más grandes y complejas.

Maldita sea ¿Y ahora que?

Recuerda lo que estaba diciendo sobre :where() y el hecho de que su especificidad es cero? Podemos usar eso a nuestro favor:

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

La primera parte de este selector (.something) obtiene su puntuación de especificidad habitual de 0,1,0. Sin embargo, :where() —y todo lo que hay dentro de él— tiene una especificidad de 0, lo que no aumenta más la especificidad del selector.

:where() nos permite anidar

Las personas a las que no les importa tanto como a mí la especificidad (y eso es probablemente mucha gente, para ser justos) lo han hecho bastante bien cuando se trata de anidar. Con algunos golpes de teclado sin preocupaciones, podemos terminar con CSS como este (tenga en cuenta que estoy usando Sass por brevedad):

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

En este ejemplo, tenemos un .card componente. Cuando se trata de una tarjeta "destacada" (usando el .card--featured clase), el título y la imagen de la tarjeta deben tener un estilo diferente. Pero, como nosotros ahora sabe, el código anterior da como resultado un puntaje de especificidad que es inconsistente con el resto de nuestro sistema.

Un nerd acérrimo de la especificidad podría haber hecho esto en su lugar:

.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }

Eso no es tan malo, ¿verdad? Francamente, este es un hermoso CSS.

Sin embargo, hay una desventaja en el HTML. Los autores experimentados de BEM probablemente son dolorosamente conscientes de la lógica de plantilla tosca que se requiere para aplicar condicionalmente clases de modificadores a múltiples elementos. En este ejemplo, la plantilla HTML necesita agregar condicionalmente el --featured clase modificadora a tres elementos (.card, .card__titley .card__img) aunque probablemente aún más en un ejemplo del mundo real. Eso es mucho if Declaraciones.

El :where() El selector puede ayudarnos a escribir mucha menos lógica de plantilla, y menos clases BEM para arrancar, sin aumentar el nivel de especificidad.

.card { ... }
.card--featured { ... }

.card__title { ... }
:where(.card--featured) .card__title { ... }

.card__img { ... }
:where(.card--featured) .card__img { ... }

Aquí está lo mismo pero en Sass (tenga en cuenta el final símbolos de unión):

.card { ... }
.card--featured { ... }
.card__title { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}
.card__img { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}

Si debe o no optar por este enfoque en lugar de aplicar clases de modificadores a los diversos elementos secundarios es una cuestión de preferencia personal. Pero al menos :where() ¡nos da la opción ahora!

¿Qué pasa con el HTML que no es BEM?

No vivimos en un mundo perfecto. A veces necesita lidiar con HTML que está fuera de su control. Por ejemplo, una secuencia de comandos de terceros que inyecta HTML que necesita diseñar. Ese marcado a menudo no se escribe con nombres de clase BEM. ¡En algunos casos, esos estilos no usan clases sino ID!

Una vez más, :where() tiene nuestra espalda. Esta solución es un poco complicada, ya que necesitamos hacer referencia a la clase de un elemento en algún lugar más arriba en el árbol DOM que sabemos que existe.

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

Sin embargo, hacer referencia a un elemento principal se siente un poco arriesgado y restrictivo. ¿Qué pasa si esa clase principal cambia o no está allí por alguna razón? Una solución mejor (pero quizás igualmente hacky) sería usar :is() en cambio. Recuerde, la especificidad de :is() es igual al selector más específico de su lista de selectores.

Entonces, en lugar de hacer referencia a una clase que sabemos (¡o esperamos!) existe con :where(), como en el ejemplo anterior, podríamos hacer referencia a una clase inventada y el etiqueta.

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

El siempre presente body nos ayudará a seleccionar nuestro #widget elemento, y la presencia del .dummy-class clase dentro de la misma :is() da el body selector la misma puntuación de especificidad que una clase (0,1,0)… y el uso de :where() asegura que el selector no sea más específico que eso.

¡Eso es!

Así es como podemos aprovechar las funciones modernas de gestión de la especificidad del :is() y :where() pseudoclases junto con la prevención de colisiones de especificidad que obtenemos al escribir CSS en un formato BEM. Y en un futuro no muy lejano, una vez :has() obtiene soporte para Firefox (actualmente se admite detrás de una bandera en el momento de escribir este artículo) probablemente querremos emparejarlo con :where() para deshacer su especificidad.

Ya sea que haga todo lo posible por nombrar BEM o no, ¡espero que podamos estar de acuerdo en que tener consistencia en la especificidad del selector es algo bueno!

Sello de tiempo:

Mas de Trucos CSS