Custom Properties do CSS: Como Usar Variáveis Nativas e Aposentar o Pré-Processador

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O Sass resolveu um problema que já não existe
Em 2012, o CSS era primitivo. Não tinha variáveis, nesting, nem funções matemáticas. O Sass surgiu como salvação.
Hoje o cenário mudou radicalmente. Custom Properties são nativas, calc() faz contas, @layer organiza cascata e o nesting nativo já funciona em todos os browsers modernos. Mesmo assim, muitos projetos ainda carregam node-sass ou dart-sass como dependência obrigatória.
Vamos corrigir isso. Neste post, você vai dominar CSS Custom Properties do básico ao avançado — e entender exatamente quando o pré-processador se torna peso morto.
O que são Custom Properties
Custom Properties são variáveis declaradas diretamente no CSS. Elas seguem a cascata, herdam valores do elemento pai e podem ser manipuladas em tempo real via JavaScript.
A sintaxe é simples. Declare com -- e consuma com var():
:root {
--color-primary: #6366f1;
--spacing-md: 1rem;
--radius-lg: 12px;
}
.button {
background: var(--color-primary);
padding: var(--spacing-md);
border-radius: var(--radius-lg);
}A diferença fundamental para variáveis do Sass? Elas existem em runtime. Variáveis Sass são resolvidas na compilação e viram valores estáticos. Custom Properties vivem no navegador e reagem a mudanças.
Essa diferença muda tudo.
Escopo e cascata: o superpoder ignorado
Variáveis Sass são globais ou locais ao arquivo. Custom Properties seguem a cascata do CSS. Isso significa que você pode redefinir valores por contexto sem criar classes extras.
:root {
--text-color: #1a1a2e;
--bg-color: #ffffff;
--surface-color: #f8f9fa;
}
.card {
color: var(--text-color);
background: var(--bg-color);
}
/* Redefinição por escopo */
.card--dark {
--text-color: #e2e8f0;
--bg-color: #1e293b;
--surface-color: #334155;
}
.card--brand {
--text-color: #ffffff;
--bg-color: #6366f1;
}Perceba: o .card não mudou. Apenas redefinimos as variáveis no escopo do modificador. Todos os filhos de .card--dark herdam os novos valores automaticamente.
Tente fazer isso com Sass. Você vai precisar de mixins, maps e uma quantidade absurda de boilerplate.
Escopo em componentes reais
Veja um exemplo prático com um sistema de alertas:
.alert {
--alert-bg: #f0f9ff;
--alert-border: #3b82f6;
--alert-text: #1e40af;
background: var(--alert-bg);
border-left: 4px solid var(--alert-border);
color: var(--alert-text);
padding: 1rem 1.25rem;
border-radius: 6px;
}
.alert--success {
--alert-bg: #f0fdf4;
--alert-border: #22c55e;
--alert-text: #166534;
}
.alert--danger {
--alert-bg: #fef2f2;
--alert-border: #ef4444;
--alert-text: #991b1b;
}
.alert--warning {
--alert-bg: #fffbeb;
--alert-border: #f59e0b;
--alert-text: #92400e;
}Uma única regra base. Variantes alteram apenas as variáveis. Zero duplicação.
Fallbacks inteligentes
A função var() aceita um segundo argumento: o valor de fallback. Essa mecânica é mais poderosa do que parece.
.component {
/* Fallback simples */
color: var(--custom-color, #333333);
/* Fallback encadeado */
background: var(--theme-bg, var(--default-bg, #ffffff));
/* Fallback com calc */
padding: var(--spacing, calc(1rem + 4px));
}Isso permite criar componentes que funcionam com ou sem um sistema de design tokens definido. O componente tenta usar o token. Se não existir, usa o fallback.
Padrão de API pública para componentes
Essa técnica cria uma "API" de customização para seus componentes CSS:
/* O componente define defaults internos */
.modal {
--_modal-width: var(--modal-width, 480px);
--_modal-padding: var(--modal-padding, 2rem);
--_modal-radius: var(--modal-radius, 16px);
--_modal-bg: var(--modal-bg, #ffffff);
width: var(--_modal-width);
padding: var(--_modal-padding);
border-radius: var(--_modal-radius);
background: var(--_modal-bg);
}O prefixo --_ indica variável privada. As variáveis sem prefixo são a API pública. Qualquer consumidor pode sobrescrever:
.page-checkout .modal {
--modal-width: 640px;
--modal-padding: 3rem;
}Limpo. Previsível. Sem !important.
Temas dinâmicos com zero JavaScript
O caso de uso mais impactante. Dark mode com Custom Properties é trivial:
:root {
--color-bg: #ffffff;
--color-surface: #f8fafc;
--color-text-primary: #0f172a;
--color-text-secondary: #475569;
--color-border: #e2e8f0;
--color-accent: #6366f1;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-surface: #1e293b;
--color-text-primary: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-border: #334155;
--color-accent: #818cf8;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}Todo o site muda. Nenhum componente precisa saber que o tema existe. Eles consomem variáveis e o sistema resolve.
Toggle manual com classe
Para dar controle ao usuário:
:root,
[data-theme="light"] {
--color-bg: #ffffff;
--color-text-primary: #0f172a;
--color-accent: #6366f1;
}
[data-theme="dark"] {
--color-bg: #0f172a;
--color-text-primary: #f1f5f9;
--color-accent: #818cf8;
}E o JavaScript para alternar:
const toggle = document.querySelector('#theme-toggle');
toggle.addEventListener('click', () => {
const current = document.documentElement.dataset.theme;
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
});
// Restaurar preferência salva
const saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.dataset.theme = saved;
}Nenhuma biblioteca. Nenhum CSS-in-JS. Nenhuma recompilação.
Manipulação via JavaScript
Aqui o Sass simplesmente não compete. Variáveis nativas são acessíveis e modificáveis em runtime.
Leitura e escrita
// Ler uma custom property
const root = document.documentElement;
const accent = getComputedStyle(root).getPropertyValue('--color-accent');
console.log(accent.trim()); // "#6366f1"
// Escrever uma custom property
root.style.setProperty('--color-accent', '#ec4899');
// Remover (volta ao valor herdado)
root.style.removeProperty('--color-accent');Exemplo prático: slider de customização
Imagine um painel onde o usuário ajusta o visual em tempo real:
<div class="controls">
<label>
Border Radius
<input type="range" min="0" max="32" value="8" id="radius-slider" />
</label>
<label>
Spacing
<input type="range" min="4" max="48" value="16" id="spacing-slider" />
</label>
</div>
<div class="preview-card">
<h3>Preview do Componente</h3>
<p>Ajuste os controles acima.</p>
</div>.preview-card {
padding: var(--user-spacing, 16px);
border-radius: var(--user-radius, 8px);
background: var(--color-surface);
border: 1px solid var(--color-border);
transition: all 0.2s ease;
}const root = document.documentElement;
document.querySelector('#radius-slider').addEventListener('input', (e) => {
root.style.setProperty('--user-radius', `${e.target.value}px`);
});
document.querySelector('#spacing-slider').addEventListener('input', (e) => {
root.style.setProperty('--user-spacing', `${e.target.value}px`);
});Customização visual em tempo real. Sem rebuild, sem re-render, sem framework.
Técnicas avançadas com calc() e color-mix()
Custom Properties brilham quando combinadas com funções CSS modernas.
Sistema de espaçamento fluido
:root {
--space-unit: 0.25rem;
--space-1: calc(var(--space-unit) * 1); /* 4px */
--space-2: calc(var(--space-unit) * 2); /* 8px */
--space-3: calc(var(--space-unit) * 3); /* 12px */
--space-4: calc(var(--space-unit) * 4); /* 16px */
--space-6: calc(var(--space-unit) * 6); /* 24px */
--space-8: calc(var(--space-unit) * 8); /* 32px */
--space-12: calc(var(--space-unit) * 12); /* 48px */
}Quer aumentar todo o espaçamento em telas grandes? Uma linha:
@media (min-width: 1280px) {
:root {
--space-unit: 0.3rem;
}
}Todo o sistema escala proporcionalmente.
Cores derivadas com color-mix()
Gerar variantes de cor sem Sass. Funciona em todos os browsers modernos:
:root {
--brand: #6366f1;
--brand-light: color-mix(in srgb, var(--brand) 30%, white);
--brand-dark: color-mix(in srgb, var(--brand) 70%, black);
--brand-subtle: color-mix(in srgb, var(--brand) 10%, transparent);
}
.button-primary {
background: var(--brand);
}
.button-primary:hover {
background: var(--brand-dark);
}
.badge {
background: var(--brand-subtle);
color: var(--brand-dark);
}Mude --brand e todas as variantes se recalculam. Isso era exclusividade do Sass até ontem.
Tipografia responsiva com clamp()
:root {
--font-size-sm: clamp(0.8rem, 0.17vw + 0.76rem, 0.89rem);
--font-size-base: clamp(1rem, 0.34vw + 0.91rem, 1.19rem);
--font-size-lg: clamp(1.25rem, 0.61vw + 1.1rem, 1.58rem);
--font-size-xl: clamp(1.56rem, 1vw + 1.31rem, 2.11rem);
--font-size-2xl: clamp(1.95rem, 1.56vw + 1.56rem, 2.81rem);
--font-size-3xl: clamp(2.44rem, 2.38vw + 1.85rem, 3.75rem);
}
h1 { font-size: var(--font-size-3xl); }
h2 { font-size: var(--font-size-2xl); }
h3 { font-size: var(--font-size-xl); }
p { font-size: var(--font-size-base); }Tipografia fluida. Sem media queries. Sem breakpoints arbitrários.
Integração com React e frameworks
Em projetos React, Custom Properties eliminam a necessidade de ThemeProvider e styled-components para temas.
Definindo variáveis inline
function Card({ accentColor, padding = '1.5rem' }) {
const style = {
'--card-accent': accentColor,
'--card-padding': padding,
};
return (
<div className="card" style={style}>
<h3 className="card__title">Título</h3>
<p className="card__body">Conteúdo do card.</p>
</div>
);
}.card {
padding: var(--card-padding, 1.5rem);
border-top: 3px solid var(--card-accent, var(--color-accent));
}
.card__title {
color: var(--card-accent, var(--color-text-primary));
}O componente recebe props de estilo sem CSS-in-JS. O CSS permanece em arquivo .css. A performance agradece.
Hook para tema dinâmico
import { useEffect } from 'react';
function useTheme(theme) {
useEffect(() => {
const root = document.documentElement;
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
return () => {
Object.keys(theme).forEach((key) => {
root.style.removeProperty(`--${key}`);
});
};
}, [theme]);
}
// Uso
function App() {
useTheme({
'color-accent': '#ec4899',
'color-bg': '#fdf2f8',
'radius-lg': '20px',
});
return <main>...</main>;
}Simples. Sem contexto, sem provider, sem re-render de toda a árvore.
Migração gradual do Sass
Você não precisa reescrever tudo. Migre incrementalmente.
Passo 1: Extraia variáveis Sass para Custom Properties
// ANTES: _variables.scss
$color-primary: #6366f1;
$color-secondary: #8b5cf6;
$spacing-md: 1rem;
// DEPOIS: _variables.scss (fase de transição)
:root {
--color-primary: #6366f1;
--color-secondary: #8b5cf6;
--spacing-md: 1rem;
}
// Manter compatibilidade temporária
$color-primary: var(--color-primary);
$color-secondary: var(--color-secondary);
$spacing-md: var(--spacing-md);Passo 2: Substitua mixins de tema
// ANTES: mixin Sass
@mixin dark-theme {
background: $dark-bg;
color: $dark-text;
}
// DEPOIS: Custom Properties (delete o mixin)
[data-theme="dark"] {
--color-bg: #0f172a;
--color-text: #f1f5f9;
}Passo 3: Avalie o que sobra
Após migrar variáveis e temas, o que resta no Sass?
- Nesting? CSS nativo suporta desde 2023.
- Loops e maps? Raro em projetos reais. Quando necessário, use PostCSS.
- Mixins complexos? Muitos viram componentes utilitários ou Custom Properties com fallback.
Se sobrar pouco, remova o Sass:
npm uninstall sass dart-sass node-sassMenos uma dependência. Menos um passo de build. Menos uma superfície de breaking changes.
Limitações reais que você deve conhecer
Custom Properties não são perfeitas. Conheça os limites.
Não funcionam em media queries. Isso não compila:
/* ❌ ISSO NÃO FUNCIONA */
:root {
--breakpoint-md: 768px;
}
@media (min-width: var(--breakpoint-md)) {
/* ... */
}Media queries são avaliadas antes da cascata. A spec não permite variáveis ali. Para breakpoints, use valores diretos ou @custom-media (ainda em draft).
Não funcionam em seletores. Você não pode interpolar variáveis em nomes de classe ou seletores.
Performance em escala massiva. Milhares de Custom Properties em :root podem impactar recálculo de estilos. Na prática, isso raramente é problema. Mas monitore com DevTools se trabalhar com design systems enormes.
Não têm tipagem forte. Qualquer string é aceita. Porém, @property resolve isso:
@property --rotation {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.spinner {
transform: rotate(var(--rotation));
transition: --rotation 0.6s ease;
}
.spinner:hover {
--rotation: 360deg;
}Com @property, o browser entende o tipo. Isso habilita transições e animações em Custom Properties — algo impossível sem essa declaração.
Minha Opinião Sincera
Eu usei Sass por quase uma década. Já defendi mixins, extends e maps em palestras. Hoje, recomendo eliminar o Sass na maioria dos projetos novos.
A verdade é dura: Sass virou overhead. Ele adiciona um passo de compilação, uma dependência com histórico de breaking changes (lembra da migração de node-sass para dart-sass?), e resolve problemas que o CSS nativo já resolve melhor.
Custom Properties são superiores em tudo que envolve temas e valores dinâmicos. Nesting nativo cobre 95% do uso de nesting do Sass. color-mix() substitui funções como darken() e lighten(). calc() existe há anos.
O que sobra para o Sass? Loops para gerar classes utilitárias. Se você precisa disso, provavelmente deveria estar usando Tailwind ou UnoCSS. E mixins complexos que, na minha experiência, geralmente indicam abstração excessiva.
CSS-in-JS? Styled-components e Emotion resolvem escopo, mas a que custo? Bundle maior, runtime overhead, hidratação mais lenta. Custom Properties com CSS Modules dão escopo sem runtime. É a combinação que uso em produção.
Tailwind? Ótima ferramenta. Mas por baixo dos panos, ele gera — adivinhe — Custom Properties para o sistema de temas. Você pode usar a mesma estratégia sem a dependência.
Minha stack atual para estilos: CSS Modules + Custom Properties + PostCSS (apenas para autoprefixer). Zero runtime. Build rápido. Manutenção simples.
Se o seu projeto ainda depende do Sass apenas para variáveis e nesting, faça um favor à sua equipe: migre. O CSS de 2024 não precisa de muletas.
Conclusão
Custom Properties transformaram o CSS de uma linguagem declarativa estática em um sistema reativo de design tokens. Elas vivem no browser, seguem a cascata, respondem a JavaScript e habilitam temas dinâmicos sem dependências.
O caminho é claro. Comece extraindo variáveis Sass para :root. Migre temas para data-theme com redefinição de variáveis. Use @property para animações tipadas. Combine com color-mix() e calc() para derivar valores.
Pré-processadores tiveram seu momento. Foram essenciais. Mas o CSS evoluiu. Reconheça isso e simplifique sua stack.
Menos dependências. Menos build steps. Mais CSS nativo. Seu projeto agradece.

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.


