Tutoriais

Função CSS light-dark(): Alternância Nativa de Tema

9 min de leitura

A implementação de modo escuro historicamente exigia uma quantidade não trivial de código padrão: uma media query prefers-color-scheme que substitui cada propriedade personalizada, JavaScript para lidar com alternâncias do usuário, localStorage para persistir preferências e um script inline no <head> para evitar o flash do tema errado no carregamento da página. A função CSS light-dark() não elimina tudo isso, mas reduz drasticamente a superfície CSS do problema.

light-dark() é uma função de cor CSS que recebe exatamente dois valores de cor e retorna o primeiro quando o esquema de cores ativo é claro, ou o segundo quando o esquema de cores ativo é escuro. É o equivalente CSS semântico do operador ternário para cores.

O que é light-dark()?

A assinatura da função é simples:

color: light-dark(<light-color>, <dark-color>);

Quando o esquema de cores ativo é claro, o navegador usa <light-color>. Quando é escuro, usa <dark-color>. O "esquema de cores ativo" é determinado pela propriedade CSS color-scheme, que por sua vez responde à media query do sistema prefers-color-scheme ou a um valor explícito definido em um elemento.

A função é suportada em:

  • Chrome/Edge: desde a versão 123 (março de 2024)
  • Firefox: desde a versão 120 (novembro de 2023)
  • Safari: desde a versão 17.5 (junho de 2024)

O suporte global é de cerca de 85% no início de 2026. É uma adição relativamente recente, mas a cobertura do navegador está crescendo rápido o suficiente para usar em produção com uma estratégia de fallback.

Como Funciona com a Propriedade color-scheme

light-dark() não funciona isoladamente. Depende inteiramente da propriedade CSS color-scheme estar configurada corretamente. Sem ela, a função não tem contexto para decidir qual valor retornar.

A propriedade color-scheme declara quais esquemas de cores um documento ou elemento suporta. Defini-la em :root é o ponto de partida:

:root {
  color-scheme: light dark;
}

Essa única declaração diz ao navegador que sua página suporta esquemas de cores claro e escuro. O navegador então:

  1. Lê a preferência de sistema prefers-color-scheme do usuário
  2. Aplica o esquema correspondente
  3. Faz todas as chamadas light-dark() na página resolverem para o valor apropriado

Com isso no lugar, definir cores conscientes do tema torna-se uma questão de escrever declarações únicas:

:root {
  color-scheme: light dark;

  --color-background: light-dark(#FFFFFF, #0F0F17);
  --color-text:       light-dark(#1A1A2E, #E8E8F0);
  --color-border:     light-dark(#DEE2E6, #2E2E4A);
  --color-accent:     light-dark(#2563EB, #60A5FA);
}

Sem media query. Sem substituições de seletor. Uma declaração por cor, ambos os valores inline. O navegador lida com a alternância automaticamente com base na preferência do sistema.

Restringindo a um Único Esquema

Definir color-scheme: light ou color-scheme: dark força um único esquema independentemente da preferência do sistema:

/* Sempre claro, independentemente da preferência do SO */
.widget {
  color-scheme: light;
  background: light-dark(#FFFFFF, #0F0F17);
  /* Sempre resolve para #FFFFFF */
}

/* Sempre escuro */
.dark-panel {
  color-scheme: dark;
  color: light-dark(#1A1A2E, #E8E8F0);
  /* Sempre resolve para #E8E8F0 */
}

Isso é útil para componentes de interface que devem sempre aparecer em um modo específico — por exemplo, um editor de código que sempre deve ter fundo escuro independentemente do tema da página ao redor.

A Palavra-chave only

Adicionar only impede que a cascata substitua o esquema para aquele elemento:

.forced-light {
  color-scheme: only light;
}

Isso é principalmente útil quando você tem um elemento dentro de um contexto de modo escuro que deve permanecer claro.

Substituindo Media Queries prefers-color-scheme

A abordagem tradicional ao modo escuro usando media queries exige duplicar ou substituir cada variável de cor:

/* Abordagem tradicional — verbosa */
:root {
  --bg: #FFFFFF;
  --text: #1A1A2E;
  --accent: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0F0F17;
    --text: #E8E8F0;
    --accent: #60A5FA;
  }
}

Com light-dark(), isso colapsa para:

/* Abordagem com light-dark() — uma declaração por variável */
:root {
  color-scheme: light dark;

  --bg:     light-dark(#FFFFFF, #0F0F17);
  --text:   light-dark(#1A1A2E, #E8E8F0);
  --accent: light-dark(#2563EB, #60A5FA);
}

As variáveis em si tornam-se autodescritivas. Quando você lê --accent: light-dark(#2563EB, #60A5FA), você vê imediatamente ambos os valores e entende a relação. A abordagem com media query dispersa os valores claro e escuro em dois blocos separados, o que torna a auditoria e a atualização da paleta mais difíceis.

Quando as Media Queries Ainda São Necessárias

A media query prefers-color-scheme ainda é necessária para adaptações não relacionadas a cores que mudam com base no tema:

@media (prefers-color-scheme: dark) {
  /* Ajustes não relacionados a cores que light-dark() não consegue expressar */
  img.logo {
    filter: invert(1) brightness(1.2);
  }

  .hero-image {
    opacity: 0.85;
  }
}

Para qualquer coisa que seja puramente uma mudança de cor, light-dark() é mais limpo. Para adaptações estruturais ou não relacionadas a cores (filtros de imagem, opacidade, propriedades de exibição), a media query permanece a ferramenta certa.

Combinando com Propriedades Personalizadas CSS

light-dark() funciona dentro de valores de propriedades personalizadas, que é onde seu poder total emerge. Você define todas as cores conscientes do tema em :root, e cada componente na página referencia essas variáveis. Quando o esquema de cores muda, tudo é atualizado simultaneamente.

Exemplo de Sistema de Tema Completo

:root {
  color-scheme: light dark;

  /* Fundos */
  --color-bg-base:      light-dark(#FFFFFF,   #0F0F17);
  --color-bg-elevated:  light-dark(#F8F9FA,   #1A1A2E);
  --color-bg-overlay:   light-dark(#F1F3F5,   #252540);

  /* Texto */
  --color-text-primary:   light-dark(#1A1A2E, #E8E8F0);
  --color-text-secondary: light-dark(#4A4A6A, #A8A8C0);
  --color-text-muted:     light-dark(#6C757D, #6A6A88);
  --color-text-inverse:   light-dark(#FFFFFF, #1A1A2E);

  /* Bordas */
  --color-border:         light-dark(#DEE2E6, #2E2E4A);
  --color-border-strong:  light-dark(#ADB5BD, #4A4A6A);

  /* Interativo / marca */
  --color-accent:         light-dark(#2563EB, #60A5FA);
  --color-accent-hover:   light-dark(#1D4ED8, #93C5FD);
  --color-accent-subtle:  light-dark(#DBEAFE, #1E3A5F);

  /* Feedback */
  --color-success:  light-dark(#16A34A, #4ADE80);
  --color-warning:  light-dark(#D97706, #FCD34D);
  --color-danger:   light-dark(#DC2626, #F87171);

  --color-success-bg: light-dark(#F0FDF4, #052E16);
  --color-warning-bg: light-dark(#FFFBEB, #2D1A00);
  --color-danger-bg:  light-dark(#FEF2F2, #2D0A0A);
}

Os componentes referenciam essas variáveis sem precisar saber nada sobre temas:

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
}

.btn-primary {
  background: var(--color-accent);
  color: var(--color-text-inverse);
}

.btn-primary:hover {
  background: var(--color-accent-hover);
}

.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-left: 3px solid var(--color-success);
}

Observe que o acento claro #2563EB muda para #60A5FA no modo escuro. Isso é intencional — o azul de peso 600 passa no contraste WCAG AA contra branco, mas falha contra fundos escuros. O de peso 400 clareia a cor o suficiente para manter contraste acessível em superfícies escuras. Use o Verificador de Contraste para verificar se cada combinação atende à sua proporção de contraste alvo, e o Gerador de Sombras para encontrar o tom certo para cada modo.

Aninhando light-dark() Dentro de Outras Funções

light-dark() retorna um valor de cor, portanto pode ser usado em qualquer lugar que uma cor seja válida — incluindo dentro de outras funções:

:root {
  color-scheme: light dark;
  --brand: #3B82F6;

  /* Usar light-dark() dentro de color-mix() */
  --brand-surface: color-mix(
    in oklch,
    var(--brand) 15%,
    light-dark(white, #09090b)
  );
}

Isso cria uma cor de superfície que é um tinte de 15% da cor da marca, misturada com branco no modo claro e quase-preto no modo escuro — automaticamente consciente do tema.

Adicionando um Alternador de Usuário (JavaScript)

Responder à preferência do sistema é o padrão correto, mas os usuários devem poder substituí-lo. Isso requer JavaScript para persistir a escolha e substituir o padrão do navegador.

Controlando color-scheme com JavaScript

O ponto central é que color-scheme é uma propriedade CSS que pode ser definida via JavaScript:

// Definir o esquema de cores programaticamente
document.documentElement.style.colorScheme = 'dark';
document.documentElement.style.colorScheme = 'light';

// Remover a substituição (reverte para a preferência do sistema)
document.documentElement.style.colorScheme = '';

Quando você define color-scheme via estilo inline no elemento raiz, ele substitui a declaração da folha de estilos. Todos os valores light-dark() resolvem novamente para a variante apropriada.

Implementação Completa do Alternador

const STORAGE_KEY = 'color-scheme-preference';

function initColorScheme() {
  const stored = localStorage.getItem(STORAGE_KEY);
  if (stored === 'light' || stored === 'dark') {
    document.documentElement.style.colorScheme = stored;
  }
  // Se não houver preferência armazenada, o CSS color-scheme: light dark; lida com isso via preferência do SO
}

function toggleColorScheme() {
  const current = getComputedStyle(document.documentElement)
    .colorScheme
    .trim();

  // Determinar o próximo valor
  const next = current.includes('dark') ? 'light' : 'dark';

  document.documentElement.style.colorScheme = next;
  localStorage.setItem(STORAGE_KEY, next);

  // Atualizar o estado do botão de alternância
  updateToggleButton(next);
}

function updateToggleButton(scheme) {
  const btn = document.getElementById('theme-toggle');
  if (!btn) return;
  btn.setAttribute('aria-label',
    scheme === 'dark' ? 'Alternar para modo claro' : 'Alternar para modo escuro'
  );
  btn.dataset.scheme = scheme;
}

// Executar antes da primeira pintura para evitar flash
initColorScheme();

// Anexar ao botão de alternância após o DOM estar pronto
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('theme-toggle')
    ?.addEventListener('click', toggleColorScheme);
});

Chamar initColorScheme() antes que o DOM seja totalmente interpretado é crítico. Se executar tarde, usuários com uma preferência armazenada verão o tema padrão do SO brevemente antes de ele mudar — o clássico "flash do tema errado". Coloque este script inline no <head> ou use o atributo defer com cuidado (note que scripts com defer são executados após a interpretação do DOM, o que pode ser tarde demais).

O Padrão Anti-Flash

A abordagem anti-flash mais robusta executa um script inline mínimo no <head>:

<head>
  <meta name="color-scheme" content="light dark">
  <script>
    const stored = localStorage.getItem('color-scheme-preference');
    if (stored) {
      document.documentElement.style.colorScheme = stored;
    }
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

Este script é executado sincronamente antes que qualquer CSS seja aplicado, para que o navegador calcule o valor correto de color-scheme desde a primeira pintura. A tag <meta name="color-scheme"> informa ao navegador quais esquemas esperar antes mesmo do CSS ser interpretado — isso evita um breve flash branco em páginas com modo escuro em alguns navegadores.

Guia de Migração de Temas Baseados em JS

Muitas implementações de modo escuro existentes usam um atributo data-theme alternado por JavaScript, com substituições de CSS com escopo para [data-theme="dark"]. A migração para light-dark() é incremental — você não precisa mudar tudo de uma vez.

Passo 1: Adicionar color-scheme ao :root

:root {
  color-scheme: light dark;
  /* As propriedades personalizadas existentes permanecem sem alteração */
}

Passo 2: Migrar Variáveis Uma a Uma

Comece com uma única variável como prova de conceito. Substitua o padrão de declaração dividida por um light-dark() unificado:

/* Antes */
:root {
  --bg: #FFFFFF;
}
[data-theme="dark"] {
  --bg: #0F0F17;
}

/* Depois */
:root {
  color-scheme: light dark;
  --bg: light-dark(#FFFFFF, #0F0F17);
}

Passo 3: Atualizar o Alternador

Mude o alternador JavaScript de definir data-theme para definir style.colorScheme:

/* Antes */
document.documentElement.setAttribute('data-theme', scheme);

/* Depois */
document.documentElement.style.colorScheme = scheme;

Passo 4: Remover Seletores data-theme

Depois que todas as variáveis forem migradas, remova os blocos CSS [data-theme="dark"].

Mantendo Ambos Durante a Transição

Você pode executar ambos os sistemas simultaneamente. Mantenha as substituições [data-theme="dark"] para quaisquer variáveis ainda não migradas. Novas variáveis usam light-dark(). O alternador JavaScript define tanto data-theme quanto style.colorScheme durante o período de transição.

Fallback de Suporte de Navegador

Para os aproximadamente 15% dos navegadores sem suporte a light-dark(), forneça fallbacks explícitos:

:root {
  /* Fallback: valores explícitos de modo claro */
  --color-bg: #FFFFFF;
  --color-text: #1A1A2E;

  /* Aprimoramento progressivo com light-dark() */
  --color-bg:   light-dark(#FFFFFF, #0F0F17);
  --color-text: light-dark(#1A1A2E, #E8E8F0);
}

/* Fallback para modo escuro em navegadores sem light-dark() */
@supports not (color: light-dark(white, black)) {
  @media (prefers-color-scheme: dark) {
    :root {
      --color-bg: #0F0F17;
      --color-text: #E8E8F0;
    }
  }
}

O bloco @supports not (color: light-dark(white, black)) só se aplica a navegadores que não entendem light-dark(). Os navegadores modernos o ignoram completamente porque a condição negativa é falsa.

Principais Conclusões

  • light-dark(<light-value>, <dark-value>) retorna o primeiro argumento em um esquema de cores claro e o segundo em um esquema escuro. É a forma nativa CSS de expressar "esta cor, adaptada para o tema atual".
  • Depende da propriedade CSS color-scheme estar definida no elemento (ou em um ancestral). Sempre defina color-scheme: light dark em :root para habilitar a adaptação automática via prefers-color-scheme.
  • A principal vantagem sobre a abordagem tradicional com media query é colocar ambos os valores de tema em uma única declaração — tornando a relação entre variantes claras e escuras explícita e auditável.
  • As substituições de usuário requerem definir document.documentElement.style.colorScheme via JavaScript. Persista a escolha em localStorage e aplique-a em um script inline no <head> antes que o CSS carregue para evitar flash.
  • A migração de sistemas baseados em atributo data-theme é incremental — mova uma variável de cada vez do padrão de substituição [data-theme="dark"] para o padrão inline light-dark().
  • O suporte do navegador é ~85% em 2026. Forneça um fallback @supports not com um bloco @media (prefers-color-scheme: dark) para ambientes mais antigos.
  • Use o Verificador de Contraste para verificar se tanto os valores de cor claro quanto os escuro em cada par light-dark() atendem aos requisitos de contraste WCAG em relação aos seus respectivos fundos, e o Gerador de Sombras para encontrar o tom certo de cada cor para cada modo.

Cores relacionadas

Marcas relacionadas

Ferramentas relacionadas