Pensando o CSS em forma de componentes

"C" de "composed" em vez de "cascading"


"Duas propriedades CSS entram num bar. Um banquinho em outro bar completamente diferente desaba."

Tenho que confessar: minha relação com CSS é… complicada.

Por um lado, é uma linguagem poderosa que permitiu que, não apenas programadores, mas todo o tipo de profissional pudesse contribuir de forma prática com estilos que enriqueceram a web todos esses anos! Sua base de cascata, o "cascading", fez com que bibliotecas pudessem ser criadas e importadas com uma tag link e, depois, facilmente adaptadas ao reutilizarmos as classes e sobrescrevermos seus estilos.

Por outro lado, surgem muitos problemas quando você cai em um projeto que não é baseado em páginas e sim em produtos. Trabalhar em um produto significa garantir consistência, reaproveitamento e corretude ao longo de todo seu desenvolvimento e reconhecer que mudanças vão ocorrer.

Há duas certezas na vida: a morte e mudanças de requisitos de software — brennovich

E então, quando tentamos aplicar conceitos de programação para deixar o código simples, escalável e modificável, o CSS se mostra um bicho difícil de domar. Não é raro encontrar produtos cuja parte mais complexa de todo o sistema seja o CSS.

Mas, afinal, o que torna o CSS uma linguagem difícil?

Há várias razões, mas gostaria de me concentrar em algumas que acho particularmente complexas e que caracterizam essa linguagem. Depois de entendermos esses problemas, vou propor uma solução.

Dependência de ordem de importação

Já tentou importar seus arquivos em uma ordem diferente? Você presencia um exemplar de arte moderna no lugar da sua página e percebe de imediato que algo de muito errado aconteceu. No entanto, este é o melhor dos casos! Os problemas podem ser muito mais sutis, como neste exemplo aqui:

// buttons.css
.submit-button {
padding: 0 10px;
background: red;
...
}
// forms.css
.submit-button {
padding: 0;
width: 100%;
}

O que acontece se trocamos a ordem de importação?

<link rel="stylesheet" type="text/css" href="buttons.css">
<link rel="stylesheet" type="text/css" href="forms.css">

Usando o box-model padrão, você (ou mais provavelmente o canal de suporte) vai perceber problemas deste tipo:

Exemplo meramente ilustrativo que nunca aconteceu :)

Quanto mais reaproveitamento de código houver, maior é a chance de casos como esse acontecerem.

Markup sem estilos e sem aviso

Em um projeto grande, códigos vêm e vão. Estilos mudam ou deixam de existir no projeto. E não é legal gastar o plano 3G dos seus usuários à toa. Por isso, enviamos apenas os estilos necessários para renderizar cada página e removemos o que não está mais sendo usado.

Então, como bom escoteiro, você busca por aquela classe CSS, remove todas as ocorrências e… um PR que estava para entrar usa exatamente a classe que você removeu. Ou quem sabe a busca no HTML pelo uso do componente não deu resultado porque a classe estava sendo gerada dinamicamente pela template engine. De qualquer maneira, o componente que faltou foi bem aquele que fica geralmente escondido até certo ponto do workflow do usuário e ninguém pegou no teste manual, confiando nos testes funcionais. Resultado: conteúdo não estilizado em produção. Ei, pelo menos é responsivo :)

Especificidade de seletores

Em determinado ponto do percurso todo front-end se cansa de repetir e copiar código CSS e começa criar classes mais genéricas, reutilizáveis; cria utils e combina classes diferentes para construir os elementos de layout. Utils como este aqui:

.to-left { display: block; float: left; }

Já identificou o problema? Tente aplicar essa classe a um elemento que recebe a seguinte propriedade:

.menu .menu-item { float: none; }

Ao aplicar a regra, nada acontece! Isso porque a primeira é menos específica. Logo, mesmo que ela seja aplicada depois da outra, não vai sobrescrever o comportamento. Uma forma de resolver seria colocar mais classes para complementar o util, como:

<div class="menu-item to-left really-to-left"> … </div>

Por favor, não faça isso. Já pensou se a especificidade for de três níveis? Quatro? Outra opção ruim: usar !important. Depois que a regra existe, desista de tentar sobrescrever.

Acho que deu pra perceber que fica difícil reutilizar CSS assim, o que nos leva a seguinte conclusão:

Escolha seu veneno: nomes pouco semânticos ou baixo reaproveitamento

Ao dar nomes de classes CSS podemos usar padrões como o BEM e deixar bem clara a relação entre as classes e seu modelo de dados, ou podemos usar técnicas como CSS atômico, OOCSS e Bootstrap e abrir mão dessa relação.

Escolhendo a primeira opção com CSS puro, a solução é simples: cada componente é escrito do zero, sem reaproveitamento.

Já usando técnicas de reaproveitamento, há um enorme esforço para que não haja colisão de regras e uma hierarquia impecável de componentes e módulos. Se (ou melhor, quando) houver algum erro nesta visão de longo prazo de todos os estilos de um projeto médio/grande, voltamos aos bons tempos de horas depurando código para descobrir por quê quebrou o layout de uma área completamente diferente da qual estamos trabalhando.


A solução: troque Cascata por Composição

Agora, você deve estar pensando: mas ninguém programa mais com o CSS das ruas, CSS puro. Os pré-processadores já não resolvem essas questões?

E a resposta, como muitas coisas na vida, é: sim e não.

Isso porque você pode resolver com pré-processadores, ao utilizar mixins.

Mixins permitem que a gente pare de usar a hierarquia de herança múltipla da Cascata do CSS, e que usemos Composição no lugar.

Usando os exemplos anteriores, podemos criar o seguinte em SASS:

@import '../utils/to-left';
@import '../components/buttons';
// Util importado
@mixin to-left { float: left; }
// Componente importado
@mixin button-submit {
padding: 0 10px;
background: red;
}
.lead-form__submit {
@include button-submit;
@include to-left;
}

Agora, vamos analisar esse código um tanto besta sob o ponto de vista dos problemas que vimos:

  1. A ordem de importação não importa mais. Ufa! O que importa é apenas a ordem de uso, como em qualquer linguagem de programação
  2. Markups sem estilos são muito mais difíceis de acontecer, porque não é mais possível falhar silenciosamente. Só acontece se você, além de não importar as dependências, esquecer de usar os estilos… Mas aí não tem técnica que nos salve :)
  3. Especificidade de seletores? O que é isso mesmo? Agora já não interessa mais, pois cada seletor declara todas as suas regras explicitamente, como se você fosse escrever o CSS do zero para cada regra. Mas, como estamos usando composição, estamos reaproveitando o que já existe. Isso sem copiar código, é claro, e sem que haja dependências entre as classes
  4. Estamos usando nomes semânticos junto com componentes. Nomes semânticos nos ajudam muito a entender o que está acontecendo no código, pois podemos ligar estilos com nosso modelo de dados real. Por exemplo: suggestions-carousel__see-more nos diz muito mais a respeito de um elemento que carousel__button

Essa técnica é tão simples quanto é poderosa.

A velocidade e a qualidade de desenvolvimento melhoram muito a partir do momento em que você só se preocupa com os estilos e quebra de layout quando está desenvolvendo um componente isolado. Se você testar um componente e prepará-lo para se adaptar em diferentes tamanhos e cenários (quase como um teste unitário), isso não será mais uma preocupação na hora de usar. A não ser que haja colisão de classes globais, incluir um componente em um lugar nunca vai quebrar outro lugar não relacionado.


Como usar componentes CSS na vida real?

Para aplicar essa técnica, mudamos a forma de pensar no CSS e também como organizamos o código, o que pode ser bastante desafiador em um projeto grande. Por isso, vou dar algumas sugestões:

  • Uma ótima forma de usar é implementar os componentes antes de usar, incluindo-os em um styleguide vivo antes. Apenas depois que ele estiver finalizado, testado e alinhado com o designer, incluímos na página. Essa é uma abordagem chamada Styleguide First e depende de alguns requisitos para funcionar bem, mas essa explicação fica para um próximo artigo
  • Um componente fica em um arquivo separado e, de preferência, na mesma pasta que seu template e JS
  • Em um mesmo arquivo, podem existir várias versões de um mesmo componente. Exemplos: button-primary-small, button-secondary, button-outline, etc.
  • Não modifique componentes depois de incluí-los. Em vez disso, crie um novo componente com as modificações que você precisa. Você pode inclusive extrair características em comum e reutilizá-las. Por exemplo, as regras de dimensão (padding, font-size, etc.) podem ficar em componentizáveis locais como _button-aspect-small e _button-aspect-large; e regras de apresentação em outros mixins, como _button-skin-primary e _button-skin-outline. A declaração dos componentes ficaria da seguinte forma:
@mixin button-primary {
@include _button-skin-primary;
@include _button-aspect-base;
}
@mixin button-outline-large {
@include _button-skin-outline;
@include _button-aspect-large;
}
...
  • Ao usar BEM, procure deixar regras internas com o escopo do componente genérico. Não as deixe dependentes da classe que incluiu o componente, para não ter que usar nomes incorretos de classes do tipo publisher-page__navbar__menu:
// Errado
@mixin navbar {
background: white;
  &__menu {
color: blue;
}
}
// Ruim (evitar)
@mixin navbar(selector) {
background: white;
  #{selector}__menu {
color: blue;
}
}
// Bom!
@mixin navbar {
background: white;
  .navbar__menu {
color: blue;
}
}
  • Mantenha uma pasta com utils, para comportamentos do tipo cover, hidden-text, ellipsis, drop-shadow, etc. Assim você garante consistência na implementação desses comportamentos, além de diminuir bastante o tamanho do CSS

O que eu ganho com isso?

Pensar o CSS em componentes ajudou bastante nosso time de Website do VivaReal a organizar nossa aplicação e permitiu que nos preparássemos para trocar o design das páginas com pouco esforço. Também conseguimos reduzir o tamanho do CSS em muitas vezes (em alguns casos, para menos de 1/10 do tamanho original) enquanto deixamos nossa parte crítica inline no HTML com o mínimo de estilos possível.

Nossos ganhos foram grandes porque, como nossa aplicação principal ainda usa Backbone, as particularidades do CSS causavam bastante impacto no nosso dia-a-dia. Ao utilizar frameworks mais novos, como o React, alguns desses problemas causam menos impacto por conta do maior isolamento das regras. Alguns usam shadow dom ou criam um escopo automaticamente, aumentando a previsibilidade. Mesmo nesses casos, esta técnica pode ser usada para desacoplar a declaração dos estilos do seu uso, o que permite que você reutilize estilos em diversos componentes sem que haja impacto algum no tamanho do payload.

Acho importante mencionar que, ao usar muitos mixins, o tamanho de seu CSS cru, sem gzip, fica maior que utilizando outras técnicas como extends. Isso não impacta muito no tamanho depois do gzip, mas pode ser um fator importante caso você esteja desenvolvendo AMPs, por exemplo, que limita o tamanho original a 50kB. Nós resolvemos esse problema usando um uglify nas nossas classes CSS, que troca os grandes nomes de classes BEM por poucas letras.


Se você chegou até aqui, experimente usar na sua aplicação! Você pode aplicar mesmo em sites legados e grandes bases de códigos. O segredo é fazer aos poucos em vez de tentar rescrever tudo. Foi o que fizemos (e ainda estamos fazendo) aqui no Viva. Depois, compartilhe aqui com a gente suas impressões e resultados.

Boa componentização!