Use variáveis CSS como uma pessoa sênior com essas 5 dicas 🔥

Camilo Micheletto - Nov 23 '23 - - Dev Community

Sumário


101 🔗

Repetição é bem comum no CSS, ainda mais em fontes, cores e espaçamentos. Repetição nem sempre é desperdício, se a gente quer ser consistente com o nosso design, repetição favorece a percepção de padronização das pessoas usuárias. No caso abaixo, temos um timing de transição padrão pras animações dos componentes:

.btn {
  transition: background 200ms ease-in;
}

.link {
  transition: color 200ms ease-in;
}
Enter fullscreen mode Exit fullscreen mode

 

Pra garantir a acessibilidade, temos que possibilitar que usuários que se distraem com animações ou possuem algum tipo de sensibilidade à movimento possam desativá-los via configuração do user-agent, que no caso é o navegador:

.btn {
  transition: background 200ms ease-in;
}

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: background linear;
  }
}

Enter fullscreen mode Exit fullscreen mode

 

Nesse caso, pra cada classe que tivesse esse timing teríamos que criar também dentro da @media pra que a pessoa usuária conseguisse mudar. E se além disso mudarmos o tema pra escuro? Teríamos que colocar todas as classes com cores dentro da @media (prefers-color-scheme: dark)?

Com variáveis CSS poderíamos fazer diferente!
Primeiro guardamos a nossa configuração de timing e easing num local que ela ficasse disponível "globalmente". Geralmente usamos :root {} que é um equivalente à html {}, mas que possuí ainda mais precedência.

:root {
  --timing: 200ms ease-in;
}
Enter fullscreen mode Exit fullscreen mode

 

E ai podemos atualizar todas referências desse valor no código usando a função var()

.btn {
  transition: background var(--timing);
}

.link {
  transition: color var(--timing);
}
Enter fullscreen mode Exit fullscreen mode

 

Só isso já nos daria a vantagem de, se caso for necessária a mudança de valor dessa propriedade, mudarmos em um lugar só, mas o pulo do gato não chegou ainda, se liga:

:root {
  --timing: 200ms ease-in;
  @media (prefers-reduced-motion: reduce) {
    --timing: linear;
  }
}
Enter fullscreen mode Exit fullscreen mode

Se a media condition (prefers-reduced-motion: reduce) retornar true, como a variável --timing: linear foi redeclarada depois, ela vai sobrescrever a declaração --timing: 200ms ease-in;. Percebe que não só economizamos linhas de CSS com isso, mas colocamos dentro do mesmo bloco toda a lógica desse comportamento?


Design tokens 🔗

Design tokens são os átomos que compõe os componentes de uma aplicação. Um botão por exemplo é feito com um tamanho de fonte específica, cores próprias e que reagem às mudanças de estado, bordas que seguem esses padrões de cores, um padding padrão nas bordas e um gap representando um gutter em botões que também tem ícones. Todas essas configurações podem ser consistentes em outros componentes e podem ser pré-estabelecidas e guardadas em variáveis - tokens.

 

Um exemplo do uso de tokens é a criação de um padrão de cores a ser usada numa aplicação. Usando variáveis CSS além de defini-las com um nome apropriado, deixando elas fáceis de reproduzir, mudar e manter, podemos altera elas facilmente em diferentes condições, como a mudança pra um tema escuro.

Abaixo um exemplo do framework de variáveis CSS OpenProps usando variáveis CSS e media features pra implementar um modo escuro.

Documentação da lib Open Props na seção Theming 101

 

No exemplo acima foi colocado o seletor :root dentro de uma media query com prefers-color-scheme: dark. Com isso, se a media condition for true, o :root contendo as variáveis com o tema escuro irá sobrescrever o anterior contendo o tema claro padrão.

Podemos criar múltiplos temas em componentes usando o mesmo sistema:


.button {
  color: var(--btn-text, #1A1A1A);
  background: var(--btn-bg, #FAFAFA);
}

.button[danger] {
  --btn-text: #FAFAFA;
  --btn-bg: #FF0000;
}

Enter fullscreen mode Exit fullscreen mode

 

No HTML:

<div>
  <button class="button">Tema claro</button>
  <button class="button" danger>Tema vermelho</button>
</div>
Enter fullscreen mode Exit fullscreen mode

 

Mas e se eu quisesse alterar um tema? Pra um elemento como um alert ou callout seria interessante adicionar um pouco de opacidade à cor de fundo, mas só isso. Como você faria?

TailwindCSS resolveu isso com composição de variáveis.


Composição de variáveis 🔗

No artigo Composing the Uncomposable with CSS Variables de Adam Wathan, o criador do TailwindCSS ele aborda justamente essa questão.

TailwindCSS é um plugin de PostCSS que gera classes utilitárias on-demand à partir de um Design System. Possuindo um DS, ele também possui os tokens que eu estava falando no tópico anterior:

Print do site do Tailwind com os tokens de imagem

 

Vamos supor que você aplica a classe .text-blue-300 que deixa a cor do texto num tom de azul e, ao mesmo tempo, aplica a classe .text-opacity-50 que deixa a cor do texto com opacidade de 50%.

A solução de Watham foi criar uma função de cor e no argumento de opacidade incluir uma variável CSS ao invés de um valor. Dessa forma, se .text-opacity- tivesse no mesmo escopo que uma classe de cor, ela "injetaria" a variável que ela contém na classe.

Abaixo, o exemplo do artigo citado acima em que a classe de opacidade "injeta" o valor de --text-opacity quando usada em um elemento junto com a classe de cor:

.text-blue-300 {
  --text-opacity: 1;
  color: rgba(144, 205, 244, var(--text-opacity));
}

.text-opacity-50 {
  --text-opacity: 0.5;
}
Enter fullscreen mode Exit fullscreen mode

 

Classes aplicadas ao mesmo elemento estão sob o mesmo escopo, ou seja, elas podem influenciar umas as outras através de variáveis CSS:

Exemplo visual de como variáveis podem influenciar classes diferentes dentro do mesmo escopo

 

Dito isso, será que é possível criar diversas variantes de um componente apenas injetando variáveis neles com classes modificadoras?

É. Bootstrap 5 fez isso com maestria.


Blocos enxutos 🔗

O componente de botão do Bootstrap possui 9 variantes, sendo uma delas um link, como eles fazem?

Print da documentação que mostra as 9 variantes de botões do bootstrap com código fonte

 

Esse é o código fonte do bootstrap/dist/css/bootstrap.css. Algumas propriedades foram omitidas por brevidade:

.btn {
  --bs-btn-padding-x: 0.75rem;
  --bs-btn-padding-y: 0.375rem;
  --bs-btn-font-family: ;
  --bs-btn-font-size: 1rem;
  --bs-btn-font-weight: 400;
  --bs-btn-line-height: 1.5;
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-bg: transparent;
  --bs-btn-border-width: var(--bs-border-width);
  --bs-btn-border-color: transparent;
  --bs-btn-border-radius: var(--bs-border-radius);
  --bs-btn-hover-border-color: transparent;
  --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
  --bs-btn-disabled-opacity: 0.65;
  --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);

  display: inline-block;
  padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
  font-family: var(--bs-btn-font-family);
  font-size: var(--bs-btn-font-size);
  font-weight: var(--bs-btn-font-weight);
  line-height: var(--bs-btn-line-height);
  color: var(--bs-btn-color);
  border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
  border-radius: var(--bs-btn-border-radius);
  background-color: var(--bs-btn-bg)
}
Enter fullscreen mode Exit fullscreen mode

 

No código acima, percebemos que:

  • São definidos valores padrão dentro do escopo do componente, inclusive valores que referenciam outras variáveis, dessa forma evitando a repetição de tokens com diferentes nomenclaturas, possibilitando uma referência cruzada.

  • Embaixo são definidas as propriedades consumindo desses tokens.

Agora vamos ver a composição da variante .btn-primary:

.btn-primary {
  --bs-btn-color: #fff;
  --bs-btn-bg: #0d6efd;
  --bs-btn-border-color: #0d6efd;
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #0b5ed7;
  --bs-btn-hover-border-color: #0a58ca;
  --bs-btn-focus-shadow-rgb: 49, 132, 253;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #0a58ca;
  --bs-btn-active-border-color: #0a53be;
  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
  --bs-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: #0d6efd;
  --bs-btn-disabled-border-color: #0d6efd;
}
Enter fullscreen mode Exit fullscreen mode

 

Só tem variáveis CSS.

Através da especificidade a variante altera os valores sem adicionar ou redeclarar nenhuma propriedade, de forma extremamente enxuta (DRY).

Agora, o que você faria se precisasse entregar um layout como o abaixo?

10 cards enfileirados em duas linhas contendo 5 cards cada. Cada card tem uma cor distinta.

 

Usando essa mesma estratégia, criamos uma classe .card em que background-color recebe uma variável e pra cada nth-child(n)a gente muda a cor da variável:

.card {
  height: var(--size); width: var(--size);
  background-color: var(--color, gray);
  border-radius: 8px;
}

.card:nth-child(1) { --color: maroon; }
.card:nth-child(2) { --color: red; }
.card:nth-child(3) { --color: purple; }
.card:nth-child(4) { --color: fuchsia; }
.card:nth-child(5) { --color: green; }
.card:nth-child(6) { --color: lime; }
.card:nth-child(7) { --color: yellow; }
.card:nth-child(8) { --color: navy; }
.card:nth-child(9) { --color: blue; }
.card:nth-child(10) { --color: aqua; }
Enter fullscreen mode Exit fullscreen mode

 

Mas se forem 20 cards, 30 cards? O CSS vai ficar enorme né? Se pelo menos tivesse como a gente passar as cores pelo elemento, como as props no React.

😈 e se eu te disser que tem como?


Variáveis como props 🔗

Diferente das outras estratégias, essa começa na marcação. Mantemos o código da variável .card, mas mudamos como inserimos o valor da cor:


<li class="card" style="--color: tomato"></li>

Enter fullscreen mode Exit fullscreen mode

 

CSS inline. Podem rir de mim, fãs de Tailwind.

 

O conceito "parece" ruim, mas não é. Primeiro que diferente de inserir declarações de CSS (propriedade e valor), sobrescrevendo o CSS local, não sobrescrevemos ou adicionamos nada no CSS do elemento, apenas valores. Dessa forma mantemos a mesma integridade do CSS da folha de estilos, ainda com um ganho de especificidade da declaração inline.

Outro ponto é que em linguagens de templating como JSX ou afins, podemos literalmente alterar os estilos de forma lógica sem, muitas vezes, a necessidade de uma solução CSS-in-JS:

Qual a diferença disso...

<Contact color="darkgreen" />
Enter fullscreen mode Exit fullscreen mode

 

Pra isso...

<div class="contact" style="--color: darkgreen">
  <!-- ... -->
</div>
Enter fullscreen mode Exit fullscreen mode

 

Ou até mesmo isso...?

<div class="contact" style={{ '--color': contact.color }}>
  <!-- ... -->
</div>
Enter fullscreen mode Exit fullscreen mode

 

Off-topic → É possível usar HTML da mesma forma que usamos props em Styled Components

 

O que te impede de fazer isso?

<button variant="primary" dark>Follow for more!</button>
Enter fullscreen mode Exit fullscreen mode

 

E isso aqui?

button[variant=primary] {
   background-color: var(--background, red);
   color: var(--background, black);

   &[dark] {
      --background: darkred;
      --color: white;
   }
}
Enter fullscreen mode Exit fullscreen mode
  • Não use se você precisar ter um código W3C compliant
  • '&' é CSS válido, viu?

 

Vamos supor que ao invés de um botão completamente limpo quiséssemos que a classe base .btn começasse com estilos padrão. Pra isso, podemos usar o segundo parâmetro da função var(), que serve como fallback caso a variável inicial não esteja declarada.

O problema dessa abordagem é que em variantes e outros estados precisamos sempre preencher com um valor padrão caso --background ou --color nunca sejam declarados:

.btn {
   background: var(--background, #FAFAFA);
   color: var(--color, #1A1A1A);
}

.btn:focus {
  outline: 1px solid var(--background, #FAFAFA);
}
Enter fullscreen mode Exit fullscreen mode

 

Existe forma melhor de fazer isso? A membro da W3C e convidada do CSS Working Group Lea Verou veio com uma abordagem muito boa sobre.


Variáveis privadas 🔗

Algumas alternativas pro problema de redeclaração de parâmetros de fallback em variáveis CSS seriam:

1 - Declaração de fallback no escopo do componente

.btn {
   --background: #FAFAFA;
   --color: #1A1A1A;
   background: var(--background);
   color: var(--color);
}

.btn:focus {
   --background: #FAFAFA;
  outline: 1px solid var(--background);
}
Enter fullscreen mode Exit fullscreen mode

Funcional, porém impede o componente de herdar --colorde um elemento pai, somente através de especificidade, o que pode se tornar difícil de gerenciar pra seletores muito complexos.

 

2 - Criação de uma variável de fallback usando o parâmetro default do var()

.btn {
   --background-initial: #FAFAFA;
   --color-initial: #1A1A1A;
   background: var(--background, var(--background-initial));
   color: var(--color, var(--color-initial));
}

.btn:focus {
   --background-initial: #FAFAFA;
  outline: 1px solid var(--background, var(--background-initial));
}
Enter fullscreen mode Exit fullscreen mode

Nessa implementação conseguimos herdar a propriedade --background e --color do elemento pai, mesmo sem uma especificidade superior, porém é bem verbosa né?

A Lea Verou cunhou o conceito de pseudo private custom properties, que funcionam similarmente à variáveis privadas em linguagens de programação (vide o sublinhado --_ no nome.

Nesse conceito criamos uma variável de fallback como anteriormente, mas diferente dessa ela terá o próprio valor padrão:

.btn {
   --_background: var(--background, #FAFAFA);
   --_color: var(--color, #1A1A1A);
   background: var(--background);
   color: var(--color);
}

.btn:focus {
  outline: 1px solid var(--_background);
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma conseguimos:

  1. Herdar as propriedades --background e --color.
  2. Definir um fallback pra cada propriedade.
  3. Fazê-lo de forma muito menos verbosa.

 


 

Todas essas estratégias me ajudaram a pensar e escrever CSS de forma muito mais enxuta e criativa, permitindo com que eu criasse layouts complexos com uma carga cognitiva menor e de forma mais extensível e customizável.

Um exemplo é esse bento layout responsivo com apenas 43 linhas de CSS:

 

Não sabe o que é um bento layout? Tá na mão.


Materiais de estudo 🔗

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .