Pressione enter para ver os resultados ou esc para cancelar.

Introdução ao CSS Modules

Importante: para reproduzir os exemplos deste artigo, é necessário ter Node e NPM instalados. Para isso, eu recomendo fortemente o NVM.

CSS Modules Logo

Sobre o que estamos falando?

Ainda hoje há uma série de problemas envolvendo a escrita de estilos CSS. Um desenvolvimento que trata essa parte com irresponsabilidade pode chegar a um código praticamente impossível de manter. Isso ocorre porque o CSS não possui nenhum tipo de solução nativa para dinamicidade da escrita e por sua permissividade em relação aos estilos globais. Nesse cenário, os efeitos colaterais podem se multiplicar e serem completamente imprevisíveis.

Grande parte das soluções propostas nos últimos anos contornaram muito bem a questão da escrita, entre elas estão as metodologias de escrita e organização (e.g. OOCSS, SMACSS, BEM) e os pré-processadores (e.g. SASS, LESS, Stylus). Apesar de facilitar muito a manutenção e o próprio dia-a-dia dos desenvolvedores, essas soluções falhavam em resolver os problemas técnicos do CSS. Ou seja, isso acabava por aumentar cada vez mais a complexidade da arquitetura de estilos, que ficava completamente agnóstica da aplicação. Eu mesmo já preguei isso.

A questão aqui é: como contornamos os problemas técnicos do CSS? Como ter manutenibilidade sem uma arquitetura desnecessariamente complexa? Como escopar e reaproveitar estilos como parte integrante da aplicação? E por último mas não menos importante: como parar de se preocupar com os side-effects?

Esses são vocês quando eu falo em CSS sem side-effects.

Pois então, a galera do CSS Modules se propõe a resolver exatamente os problemas funcionais que viemos enfrentando, pelo menos em aplicações Javascript componentizadas (e.g. React, Angular, Ember). E o melhor: com os problemas, vão embora também várias más práticas, algumas induzidas pelas soluções de escrita supracitadas.

Mas e como isso acontece?

Basicamente, os estilos são escopados por padrão. E o que isso quer dizer? Melhor explicar com um exemplo. Nesse exemplo vou usar um sistema de build chamado Webpack para compilar uma aplicação estática. Segue o link para o exemplo completo no Github (branch introduction).

Iniciar o projeto:

npm init # Passar argumento --y para criar com opções default.

Instalar webpack global e local:

npm install webpack -g
npm i -D webpack

Criar diretórios para código escrito e código compilado:

mkdir src dist

O diretório src é onde vamos desenvolver de fato, e tudo o que for escrito vai ser compilado e guardado no diretório dist, que é o build da aplicação e o que vai ser consumido pelo navegador. Agora o Webpack precisa saber disso, então criamos o arquivo de configuração dele, chamado webpack.config.js, na raiz do projeto, contendo:

module.exports = {
  entry: './src',
  output: {
    path: 'dist',
    filename: 'bundle.js',
  },
};

Agora vamos criar um arquivo index.html na raiz e adicionar o arquivo js que será gerado pelo Webpack na pasta dist, o bundle.js:

<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <title>CSS Modules Example</title>
</head>
<body>
    <h1>CSS Modules is Awesome</h1>
    <h2>Everyone should try it</h2>
    <script src="dist/bundle.js"></script>
</body>
</html>

Para testar se o Webpack está funcionando corretamente, basta criar um arquivo index.js na pasta src com o seguinte:

alert(‘deu certo!’);

Após rodar o comando webpack na raiz, o arquivo dist/bundle.js deverá conter o código do arquivos src/index.js. Isso quer dizer que, quando abrirmos o arquivo index.html no navegador, uma mensagem de alerta deverá ser exibida com a mensagem ‘deu certo!’.

Vai demorar muito?

Estamos quase lá. Mas antes, precisamos de um loader, que vamos usar com os arquivos CSS. Neste exemplo vamos usar o babel-loader, com o babeljs, que já nos permitirá usar os recursos do ES2015.

Essa imagem representa todos nós quando podemos escrever com os novos recursos do JS.
npm i -D babel-loader babel-core babel-preset-es2015

Num arquivo chamado .babelrc ficará declarada a versão que estaremos usando:

{
  "presets": ["es2015"]
}

Também é necessário adicionar a configuração do Babel ao webpack.config.js, que deverá ficar assim:

module.exports = {
  entry: './src',
  output: {
    path: 'dist',
    filename: 'bundle.js',
  },
  module: {
    loaders: [
      {
        test: /\.js/,
        loader: 'babel',
        include: __dirname + '/src',
      }
    ],
  }
};

Precisaremos agora de uma extensão do webpack para compilar o CSS, o extract-text, e outra para carregálo, o css-loader.

npm i -D extract-text-webpack-plugin css-loader

O extract-text também deve ser adicionado ao webpack.config.js, que ficará assim:

module.exports = {
  entry: './src',
  output: {
    path: 'dist',
    filename: 'bundle.js',
  },
  module: {
    loaders: [
      {
        test: /\.js/,
        loader: 'babel',
        include: __dirname + '/src',
      },
      {
        test: /\.css/,
        loader: ExtractTextPlugin.extract("css")
      }
    ],
  },
  plugins: [
    new ExtractTextPlugin("styles.css")
  ]
};

Não esquecer de chamar o CSS gerado no index.html:

<head>
  <meta charset="UTF-8">
  <title>CSS Modules Example</title>
  <link rel="stylesheet" href="dist/styles.css">
</head>

Agora vai!

Finalmente vamos escrever um pouco de CSS, no arquivo src/main.css:

.button {
  background-color: red;
  color: white;
  border-radius: 8px;
  padding: 1rem;
  cursor: pointer;
}

E carregamos esse arquivo no src/index.js:

import styles from './main.css'

let button = `<a class="button">Click</a>`

document.write(button)

Agora, rodando o comando webpack e atualizando a página no navegador já será possível ver o botão com os estilos acima.

Complexidade da Arquitetura

Vamos a um exemplo: separar os elementos html escritos pelo Javascript em arquivos diferentes. Para isso, serão criados os arquivos src/list.js e src/list.css com o seguinte código, respectivamente:

import styles from './list.css'

let list = `
  <ul class='my-list'>
    <li>I love CSS</li>
    <li>You love CSS</li>
    <li>We love CSS</li>
  </ul>
`
document.write(list)
.my-list {
  color: white;
  background-color: blue;
}

Para que o código desse arquivo seja executado, basta importar no src/index.js, que vai ficar assim:

import './list.js'
import styles from './main.css'

let button = `<a class="button">Click</a>`
document.write(button)

O exemplo acima serve para demonstrar uma aplicação componentizada. Imagine uma organização de componentes mais robusta, com vários arquivos com seus componentes dentro, separando inclusive por pastas. Cada componente vai levar seu CSS junto, o que quer dizer que não haverá mais uma complexidade desnecessária na arquitetura de estilos, ela vai apenas seguir a complexidade da própria aplicação. É por isso que CSS Modules funciona tão bem com React, por exemplo.

Escopamento

Talvez a parte mais benéfica de usar essa stack. É possível configurar o extract-text para manipular os nomes das classes e transformá-los em únicos. Primeiro vamos ver como funciona, depois analisaremos os benefícios.

Primeiro, mais uma alteração no webpack.config.js:

{
 test: /\.css/,
 loader:   ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]'),
}

Agora uma altereção na sintaxe de como escrevemos as classes nas variáveis do markup:

let button = `<a class="${styles.button}">Click</a>`

let list = `
  <ul class='${styles.myList}'>
    <li>I love CSS</li>
    <li>You love CSS</li>
    <li>We love CSS</li>
  </ul>
`

Repare que o nome da classe da lista trocou de uma sintaxe de separação por traços para uma sintaxe lowerCamelCase (my-list -> myList), então lembre-se de alterar também no CSS da lista.

Rodando o comando webpack e atualizando o navegador mais uma vez, já é possível ver que os nomes das classes estão sendo escritos de maneira diferente. Cada uma vai ter o nome do arquivo, o nome definido na declaração da classe e uma hash escrita em base 64.

Mas pra que serve isso?

Para demonstrar, vamos criar outro componente, no arquivo src/otherList.js e também seus respectivos estilos no arquivo otherList.css.

import styles from './otherList.css'

let otherList = `
 <ul class='${styles.myList}'>
    <li>I still love CSS</li>
    <li>You still love CSS</li>
    <li>We still love CSS</li>
 </ul>
`
document.write(otherList)
.myList {
 color: red;
 background-color: yellow;
}

E não esquecer de importar no src/index.js:

import './otherList.js'

Vou assumir que você já entendeu que é pra rodar o comando webpack e vou direto à parte importante. Atualizando a página, já percebemos que os estilos das listas estão diferentes. Mas não é só isso. Se você reparar, escrevemos a mesma classe nas duas listas (myList). É aí que está a mágica: como já vimos, não é assim que a classe será impressa no html e tampouco é assim que será escrito o CSS.

Ao inspecionar os elementos no navegador, é possível ver que uma lista ficou com a classe list__myList___2W1mp e a outra com a classe otherList__myList___vkli_, mesmo que tenhamos escrito apenas myList para ambas. Isso não é muito bonito, mas não importa, afinal não precisamos saber de nada disso ao escrever.

Isso possibilita, finalmente, que tenhamos estilos completamente escopados e independentes. Repare que, usando o mesmo nome de classe ao escrever os componentes, isso não é propagado a outros components, o que, como eu disse, elimina os side-effects. Além disso, não precisamos mais quebrar a cabeça ao escrever e organizar o CSS. Nada mais de classes do tipo “editable-todo-list-one-more-word-so-the-class-will-not-match-any-other“.

Sua reação ao ver que realmente dá pra ter CSS escopado e sem side-effects.

E como reaproveitar?

Já vimos que é possível escrever CSS exclusivo para cada componente, mas e como é possível reaproveitar estilos entre diversos componentes?

Isso é um assunto realmente delicado quando se fala em CSS Modules. Ter um workflow baseado em reaproveitamento de estilos globais num sistema que induz ao escopamento por padrão chega a ser contraditório. O ideal é que se aproveitem os componentes em si, mas isso é outro assunto. O que importa é que é possível reaproveitar estilos entre as classes, e é bastante fácil:

.listBase {
  border: 5px solid black;
  font-size: 30px;
}

.myList {
  composes: listBase;
  color: red;
  background-color: yellow;
}

Inclusive de outros arquivos. Criando o arquivo src/base.css e utilizando seus estilos no arquivo src/otherList.css temos, respectivamente:

.base {
  width: 50%;
  line-height: 2em;
  margin: 2rem auto;
}
.listBase {
  border: 5px solid black;
  font-size: 30px;
}

.myList {
  composes: listBase;
  composes: base from './base.css';
  color: red;
  background-color: yellow;
}

Então é isso

Já aprendemos como usar o CSS Modules de forma simples num projeto e também vimos seus principais benefícios. A partir daí não há limites! Obrigado por ler e por compartilhar (;


Ajude a construir o próximo post!

Sabemos que não acaba por aqui, mas eu quero saber de vocês: para onde faz mais sentido evoluir agora? Explorar mais o reaproveitamento? Exemplificar o uso do CSS Modules dentro de uma aplicação dinâmica (e.g React)? Adicionar mais recursos à escrita? Enfim, as possibilidades são muitas, e sugestões são sempre bem vindas.