Stash da Ana
cssjavascriptdark-modetutorial

Detectando o tema do sistema com prefers-color-scheme

2 min de leitura

Quando o sistema operacional está em modo escuro, ele expõe essa preferência para os aplicativos — incluindo o browser. O browser, por sua vez, repassa o valor para o CSS e o JavaScript através da media feature prefers-color-scheme. Isso permite que um site escolha o tema certo já na primeira renderização, sem esperar o usuário tocar em nenhum botão.

A preferência vem do OS — macOS em "Aparência", Windows em "Personalização > Cores" — e o browser apenas relê esse valor.

A media query no CSS

@media (prefers-color-scheme: dark) {
  body {
    background: #1a1a1a;
    color: #f0f0f0;
  }
}

Quando o sistema está em modo escuro, esses estilos entram. Quando o usuário troca o tema do sistema com a página aberta, o CSS reaplica em tempo real, sem reload.

A spec define três valores: light, dark e no-preference. Na prática, browsers atuais sempre reportam light ou darkno-preference virou letra morta. Não vale escrever regras para ele.

Lendo o valor no JavaScript

Para reagir no código — por exemplo, num componente React que precisa do tema antes da primeira renderização — a entrada é window.matchMedia:

const mq = window.matchMedia('(prefers-color-scheme: dark)')
console.log(mq.matches) // true se o sistema estiver em modo escuro

matchMedia devolve um MediaQueryList. .matches é o booleano atual; addEventListener('change', ...) permite ouvir mudanças enquanto a página está aberta:

mq.addEventListener('change', (event) => {
  const novo = event.matches ? 'dark' : 'light'
  console.log('Sistema mudou para:', novo)
})

O blog não escuta esse evento de propósito: se o usuário já escolheu um tema manualmente, faz mais sentido respeitar essa escolha do que sobrescrever no meio da leitura.

DevTools com a opção "Emulate CSS media feature prefers-color-scheme" Em Chrome DevTools > Rendering, dá para forçar dark ou light sem precisar mudar o tema do sistema — útil para testar.

Como o blog combina com localStorage

A leitura do tema acontece uma única vez, na montagem, e mistura duas fontes:

useEffect(() => {
  const stored = localStorage.getItem('theme') as Theme | null
  const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches
    ? 'dark'
    : 'light'
  const resolved = stored ?? preferred
  setTheme(resolved)
  document.documentElement.classList.toggle('dark', resolved === 'dark')
}, [])

A escolha explícita do usuário (no localStorage) vence; a preferência do sistema é o fallback quando ele nunca tocou no botão. A parte de persistência tem um post separado sobre localStorage; aqui interessa só a metade matchMedia.

Por que isso importa

Sem prefers-color-scheme, todo site abre num tema fixo — geralmente claro, porque a maioria dos designs ainda parte daí. Um usuário que prefere escuro precisa clicar no botão toda vez que entra num site novo. Ler a preferência do sistema elimina esse passo: a interface já chega no estado esperado.