Sumário
- 101
- Design tokens
- Composição de variáveis
- Blocos enxutos
- Variáveis como props
- Variáveis privadas
- Materiais de estudo
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;
}
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;
}
}
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;
}
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);
}
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;
}
}
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.
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;
}
No HTML:
<div>
<button class="button">Tema claro</button>
<button class="button" danger>Tema vermelho</button>
</div>
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:
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;
}
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:
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?
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)
}
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;
}
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?
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; }
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>
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" />
Pra isso...
<div class="contact" style="--color: darkgreen">
<!-- ... -->
</div>
Ou até mesmo isso...?
<div class="contact" style={{ '--color': contact.color }}>
<!-- ... -->
</div>
O que te impede de fazer isso? E isso aqui?Off-topic → É possível usar HTML da mesma forma que usamos props em Styled Components
<button variant="primary" dark>Follow for more!</button>
button[variant=primary] {
background-color: var(--background, red);
color: var(--background, black);
&[dark] {
--background: darkred;
--color: white;
}
}
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);
}
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);
}
Funcional, porém impede o componente de herdar --color
de 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));
}
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);
}
Dessa forma conseguimos:
- Herdar as propriedades
--background
e--color
. - Definir um fallback pra cada propriedade.
- 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.
Bento Grid layout responsivo em 40 linhas de CSS🥢 🍱
Camilo Micheletto ・ Nov 17
Materiais de estudo 🔗
Composing the Uncomposable With CSS Variables - Adam Watham - artigo, em inglês
Custom properties with defaults - Lea Verou - artigo, em inglês
Using CSS custom properties like this is a waste - Kevin Powell - vídeo, em inglês
Bootstrap custom properites - Bootstrap Documentation - artigo, em inglês
Getting started with CSS Custom Properties - Andy Bell - artigo, em inglês
Variáveis CSS, um guia prático - Tárcio Zemel - artigo, em português