Custom properties e tema
Pule esta página se você já sabe…
- que
--minha-coré uma propriedade CSS de verdade, e que ela herda; - a diferença entre
var(--x)evar(--x, fallback); - por que sobrescrever numa subárvore muda só aquele pedaço da tela;
- por que uma custom property vence a variável de pré-processador.
O problema
Um app com a cor da marca escrita em quarenta arquivos:
.botao {
background: #3b82f6;
}
.link {
color: #3b82f6;
}
.badge {
border-color: #3b82f6;
}
Trocar a marca é quarenta edições. Suportar tema escuro é quarenta pares de edições. E um cliente que quer a própria cor é impossível sem rebuild.
A solução tem uma linha
Custom property é uma propriedade CSS como qualquer outra — só que o nome começa com
-- e o valor é seu:
:root {
--cor-marca: #3b82f6;
}
.botao {
background: var(--cor-marca);
}
Agora a marca mora num lugar só. Mas o que faz isso ser a estratégia inteira de tema não é a centralização — é a herança.
Um exemplo completo
Arquivo único. Abra no browser: os dois cartões são o mesmo CSS, e um deles é escuro.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<title>Custom properties</title>
<style>
:root {
--fundo: #ffffff;
--texto: #101828;
--marca: #3b82f6;
--raio: 8px;
}
[data-tema="dark"] {
--fundo: #0b0d12;
--texto: #f1f3f8;
--marca: #7aa2ff;
}
.cartao {
background: var(--fundo);
color: var(--texto);
border: 1px solid var(--marca);
border-radius: var(--raio);
padding: 16px;
margin-bottom: 12px;
font-family: system-ui, sans-serif;
}
.cartao button {
background: var(--marca);
color: var(--fundo);
border: 0;
border-radius: var(--raio);
padding: 8px 16px;
}
</style>
</head>
<body>
<div class="cartao">
<p>Claro — usa os valores do <code>:root</code>.</p>
<button type="button">Ação</button>
</div>
<div class="cartao" data-tema="dark">
<p>Escuro — mesmas regras, valores sobrescritos aqui.</p>
<button type="button">Ação</button>
</div>
</body>
</html>
Nenhuma regra de .cartao foi duplicada. O segundo cartão redefine três valores no
próprio elemento, e tudo abaixo dele — inclusive o <button>, que a regra
.cartao button estiliza — passa a resolver var(--marca) para o valor novo.
Por que isso funciona
Custom properties herdam. var(--marca) não é "leia a variável global": é "leia o
valor de --marca neste elemento", e se ele não a definiu, o valor vem do pai,
e do pai do pai, até o :root.
É essa cadeia que torna o tema por subárvore possível. E é por isso que uma
variável de Sass não faz o mesmo: $marca é resolvida na hora do build e
desaparece do CSS. Custom property existe em tempo de execução — o JS lê e escreve.
// leitura
getComputedStyle(document.documentElement).getPropertyValue("--marca");
// escrita
document.documentElement.style.setProperty("--marca", "#e11d48");
var() com fallback
O segundo argumento é o valor usado quando a propriedade não está definida:
.cartao {
padding: var(--cartao-padding, 16px);
}
Isso cria um knob: quem consome pode definir --cartao-padding e mudar o
espaçamento, e quem não define ganha o 16px. Diferente de um token — que é uma
decisão de design nomeada, sempre definida — o knob existe justamente para ser
opcional.
O linter de CSS do SDK trata os dois de forma diferente
tempest doctor acusa var(--nao-existe) sem fallback como token inexistente,
e nunca reporta var(--x, fallback). Essa regra saiu do dogfood: a primeira
versão da análise acusou 47 problemas no CSS do próprio SDK, e 43 eram o idioma
de knob. Taxa de falso positivo decide se a ferramenta é lida ou ignorada.
Onde isso aparece no SDK
Esta é a estratégia inteira de tema do tempest-react-sdk. Não há prop de cor,
não há objeto de tema em JS, não há provider obrigatório para estilo: existem
tokens --tempest-* definidos no :root e redefinidos sob
[data-tempest-theme="dark"].
:root {
--tempest-bg: #ffffff;
--tempest-surface: var(--tempest-gray-50);
--tempest-text: var(--tempest-gray-900);
--tempest-primary: var(--tempest-primary-500);
}
[data-tempest-theme="dark"] {
--tempest-bg: #0b0d12;
--tempest-text: #f1f3f8;
}
Customizar o app inteiro é sobrescrever no seu próprio CSS, importado depois:
:root {
--tempest-primary: #e11d48;
--tempest-radius-md: 4px;
}
Pronto — todo <Button>, <Badge>, <Input> e o resto repintam. Você não tocou em
seletor nenhum, então não há briga de especificidade (CSS: cascata).
Escuro por atributo, não por classe
O SDK usa data-tempest-theme="dark", não class="dark". O motivo é o
exemplo desta página: como o atributo pode ficar em qualquer elemento, você
escurece uma subárvore — um preview, uma gaveta, um painel — sem escurecer a
página. Uma classe global não daria isso de graça.
O <ThemeProvider> gerencia o atributo, e themeInitScript o aplica antes
da primeira pintura, para a página não piscar branco. Detalhes em
Tema.
Token de texto validado contra um fundo não vale sobre outro
--tempest-text-subtle é resolvido contra --tempest-bg e --tempest-surface,
e reprova 4,5:1 sobre --tempest-primary-soft. Sobre superfície tingida,
use o foreground daquela superfície (--tempest-primary-on-soft) e
de-enfatize por tamanho, não por cor. Isso aconteceu duas vezes no SDK e as
duas só apareceu em browser real — o axe em jsdom desliga a checagem de
contraste porque não há pintura.
Recap
- Custom property é propriedade CSS de verdade:
--xherda do pai até o:root. ✅ - Sobrescrever numa subárvore muda só aquele pedaço da tela — é o que torna o tema por região possível.
- Ela existe em runtime; o JS lê com
getPropertyValuee escreve comsetProperty. Variável de Sass some no build. var(--x, fallback)é um knob opcional;var(--x)é um token que deve existir.- No SDK: tokens
--tempest-*no:root, tema escuro em[data-tempest-theme="dark"], e customizar é sobrescrever token — nunca brigar com seletor.
📚 Referência canônica: MDN — Custom properties
➡️ Próxima página: JavaScript: valor, referência, escopo