DEV Community

Cover image for CSS Modules en Vue.js - ¿Qué son y por qué deberíamos empezar a usarlos?
Adrian Benavente
Adrian Benavente

Posted on • Edited on

CSS Modules en Vue.js - ¿Qué son y por qué deberíamos empezar a usarlos?

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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%;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 composes2:

.serifFont {
  font-family: Georgia, serif;
}

.display {
  composes: serifFont;
  font-size: 30px;
  line-height: 35px;
}
Enter fullscreen mode Exit fullscreen mode

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]'
            }
          }
        ]
      }
    ]
  }
} 
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Invitame un café en cafecito.app


  1. GitHub - css-modules/css-modules: Documentation about css-modules 

  2. What are CSS Modules and why do we need them? 

  3. Vue.js CSS Styles Scoped vs Module | Lua Software Code 

  4. The State of CSS 2020: CSS-in-JS 

Top comments (0)