En la teoría...
Según su definición oficial1, los CSS Modules son archivos CSS que, con la ayuda de bundlers como Webpack, Parcel o Browserify, nos permiten escribir estilos que luego se convertirán en nombres de clase únicos e irrepetibles, compuestos por nombre del archivo, class name y un hash aleatorio, en cuya generación no intervendremos ni nos interesa saber cómo ocurre, aunque, para quienes tengan curiosidad, les dejo la referencia: GitHub - css-modules/icss: Interoperable CSS — a standard for loadable, linkable CSS. Cabe aclarar que no se trata de una especificación técnica dentro de CSS, si no de un paso en el "building" de nuestra aplicación.
En la práctica...
Cuando estamos trabajando en Vue, al encontrarse con un módulo CSS, el bundler tomará los nombres de clase que hayamos declarado y les agregará el nombre del componente delante, y el ya mencionado hash al final, asegurando así un encapsulamiento de estilos completo, quedando de esta forma: .{NombreDelComponente}_{nombreDeClase}_{hash}
.
Un ejemplo sencillo, con fines puramente ilustrativos, de cómo se implementa:
<!-- MiComponente.vue -->
<template>
<div :class="$style.container">
<h2 :class="$style.title">Título</h2>
<h3 :class="$style.subtitle">Subtítulo</h3>
<p :class="$style.paragraph">
Lorem ipsum dolor sit amet... etc...
</p>
</div>
</template>
<script>
...
</script>
<style module>
.container {
max-width: 1200px;
margin: auto;
}
.title {
font-weight: bold;
}
.subtitle {
color: darkgray;
font-style: italic;
}
.paragraph {
font-family: Helvetica, arial, sans-serif,
line-height: 130%;
}
</style>
El HTML final sería algo como esto...
<div class="MiComponente_container_h342kj">
<h2 class="MiComponente_title_g0921s">Título</h2>
<h3 class="MiComponente_subtitle_a298vn">Subtítulo</h3>
<p class="MiComponente_paragraph_y030mw">
Lorem ipsum dolor sit amet... etc...
</p>
</div>
Y los estilos...
.MiComponente_container_h342kj {
max-width: 1200px;
margin: auto;
}
.MiComponente_title_g0921s {
font-weight: bold;
}
.MiComponente_subtitle_a298vn {
color: darkgray;
font-style: italic;
}
.MiComponente_paragraph_y030mw {
font-family: Helvetica, arial, sans-serif,
line-height: 130%;
}
Aquí ya comienzan a asomar sus ventajas, permitiéndonos abstraernos por completo del resto de nuestra aplicación y empezar a pensar en cada componente como un universo aparte.
Preprocesadores
También se puede utilizar con preprocesadores (por ej.: <style lang="scss" module>
), en cuyo caso debemos tener en cuenta que la anidación está permitida siempre y cuando respetemos la sintáxis de Vue para CSS Modules a la hora de acceder a nuestras clases (<h2 :class="$style.title">
). De lo contrario, no funcionaría:
<template>
<div :class="$style.container">
<h2 class="title">Título</h2>
</div>
<template>
<script>
...
</script>
<style lang="scss" module>
.container {
max-width: 1280px;
margin: auto;
.title { /* ❌ ¡será ignorada! */
font-weight: bold;
}
}
</style>
Múltiples clases
Siempre que estemos usando "binding" de clases en Vue y necesitemos asignar varias a un mismo elemento, debemos hacerlo en forma de array. Pues bien, con CSS modules no es la excepción: <button :class="[$style.clase1, $style.clase2, $style.clase3, ...]">
.
La propiedad 'composes'
Sin duda una feature muy útil de los CSS Modules es la posibilidad de heredar reglas de otro selector, de manera muy similar al @extend
de Scss, utilizando la propiedad composes
2:
.serifFont {
font-family: Georgia, serif;
}
.display {
composes: serifFont;
font-size: 30px;
line-height: 35px;
}
Configuración de Webpack
Si nuestro proyecto usa una configuración manual de Webpack, los CSS Modules se pueden habilitar simplemente añadiendo la siguiente regla:
{
module: {
rules: [
// ... otras reglas
{
test: /\.css$/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[local]_[hash:base64:8]'
}
}
]
}
]
}
}
En el caso de los frameworks más comunes, suelen venir habilitados por defecto.
Naming de clases
Aunque nada nos impide definir clases en kebab case (.mi-clase-kebab-case
), por convención se recomienda utilizar siempre camel case (.miClaseCamelCase
), ya que esto permite acceder a la misma desde el template utilizando notación de puntos ($style.miClaseCamelCase
), de lo contrario deberíamos usar sintáxis de corchetes que es menos legible ($style['mi-clase-kebab-case']
).
En el JS
Como podemos intuir, cada vez que declaramos una clase, esta se convierte en una propiedad del objeto $style
que se añade al scope global al estar trabajando con CSS Modules. La misma guardará como valor el nombre único auto generado, de manera tal que si, por alguna razón, quisiéramos acceder a la misma desde el JS del componente, podríamos hacer lo siguiente:
document.querySelector(`.${this.$style.nombreDeMiClase}`);
Diferencia con "Scoped"
Si bien ambos cumplen la misma función, scoped tiene una serie de desventajas con respecto a CSS Modules:
Legibilidad
Para encapsular, Vue utiliza el atributo data
seguido de -v-
y un hash único por componente en cada elemento, lo que ensucia mucho los HTML y CSS finales3:
<template>
<div>
<h1>Title <small>(small)</small></h1>
<div>
<div class="test">Test</div>
</div>
<p>Content 1</p>
<p>Content 2</p>
</div>
</template>
<style scoped>
h1 {
font-weight: bold;
}
.test {
color: red;
}
</style>
HTML generado
<div data-v-5b2d5ecc="">
<h1 data-v-5b2d5ecc="">Title <small data-v-5b2d5ecc="">(small)</small></h1>
<div data-v-5b2d5ecc="">
<div data-v-5b2d5ecc="" class="test">Test</div>
</div>
<p data-v-5b2d5ecc="">Content 1</p>
<p data-v-5b2d5ecc="">Content 2</p>
</div>
Como vemos, con esta estrategia se agregan los atributos data
incluso en aquellos elementos que no llevan CSS 👎.
CSS generado
h1[data-v-5b2d5ecc] {
font-weight: bold;
}
.test[data-v-5b2d5ecc] {
color: red;
}
Especificidad
Por más que, a nivel de componente, los estilos están encapsulados de forma segura, podrían existir, en el HTML final, nombres de clase repetidos, lo que puede resultar confuso de leer si no se sigue un patrón de class naming como puede ser BEM o SMACSS.
Compatibilidad
Por último, los estilos de tipo scoped
son específicos de Vue, mientras que CSS Modules son un estándar y pueden ser utilizados en cualquier proyecto JS que use bundlers como Webpack, Parcel o Browserify.
Comparativa con React
Por último, solo a modo de comparación, un breve vistazo a su implementación en React, donde podemos ver que la misma es todavía más similar a la descrita en la documentación oficial1:
CSS
/* MiComponente.module.css */
.title {
font-weight: 900;
text-decoration: underline;
}
.paragraph {
font-family: Roboto, arial, sans-serif;
}
Importante: el archivo de estilos debe llevar .module antes de la extensión (por ej.: MiComponente.module.css
ó MiComponente.module.scss
).
JSX
// MiComponente.jsx
import React from "react";
import styles from "MiComponente.module.css";
const MiComponente = () => (
<>
<h2 className={styles.title}>Título</h2>
<p className={styles.paragraph}>
Lorem ipsum dolor sit amet...
</p>
</>
);
export default MiComponente;
Conclusión
CSS Modules es una librería que ha estado adquiriendo mucha popularidad en los últimos tiempos4 ya que integra CSS con Javascript como nunca antes se vió, y soluciona el problema del specificity hell, haciendo nuestro flujo de trabajo mucho más productivo.
Afortunadamente, su curva de aprendizaje es ínfima y, sin duda, es una herramienta que vale la pena añadir a nuestro stack.
Top comments (0)