Ir para o conteúdo
React

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

Marcos Soares
Atualizado em 
10 minutos de leitura
Ilustração 3D de painéis de vidro translúcido empilhados com luz índigo representando CSS Custom Properties
Ouça este artigo
0:00Custom 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():

CSS
: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.

CSS
: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:

CSS
.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.

CSS
.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:

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:

CSS
.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:

CSS
: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:

CSS
: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:

JAVASCRIPT
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

JAVASCRIPT
// 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:

HTML
<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>
CSS
.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;
}
JAVASCRIPT
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

CSS
: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:

CSS
@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:

CSS
: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()

CSS
: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

JSX
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>
  );
}
CSS
.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

JAVASCRIPT
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

SCSS
// 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

SCSS
// 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:

Bash
npm uninstall sass dart-sass node-sass

Menos 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:

CSS
/* ❌ 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:

CSS
@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.

Marcos Soares

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.