CSS Modules en Vue.js - ¿Qué son y por qué deberíamos empezar a usarlos?
Tags: css, vue
Published: 2020-12-29 03:30:59

## En la teoría...

Según su definición oficial[^1], 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](https://github.com/css-modules/icss). 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:

```html
<!-- 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...

```html
<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...

```css
.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:

```html
<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]:

```css
.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:

```js
{
  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:

```js
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 finales[^3]:

```html
<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

```html
<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

```css
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 oficial[^1]:

### CSS

```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

```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 tiempos[^4] 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.

[^1]: [GitHub - css-modules/css-modules: Documentation about css-modules](https://github.com/css-modules/css-modules)

[^2]: [What are CSS Modules and why do we need them?](https://css-tricks.com/css-modules-part-1-need/)

[^3]: [Vue.js CSS Styles Scoped vs Module | Lua Software Code](https://code.luasoftware.com/tutorials/vuejs/vuejs-css-styles-scoped-vs-module/)

[^4]: [The State of CSS 2020: CSS-in-JS](https://2020.stateofcss.com/en-US/technologies/css-in-js/#css_in_js_experience_ranking)

[![Invitame un café en cafecito.app](https://cdn.cafecito.app/imgs/buttons/button_1.svg)](https://cafecito.app/fena)