Modo escuro no Next.js: guia completo com next-themes

Na primeira vez que implementei modo escuro em um projeto Next.js, tive uma péssima experiência. No instante em que a página carregava, aparecia um clarão branco antes da mudança para o modo escuro — aquele flash era enlouquecedor. Só percebi a gravidade do problema quando um usuário comentou que a tela quase tinha cegado seus olhos.
Depois disso, testei várias soluções: escrevi minha própria implementação, usei a biblioteca use-dark-mode e consultei inúmeros tutoriais. No fim, descobri que next-themes era a verdadeira salvação. Hoje uso a biblioteca em todos os projetos: não há flashes, a configuração é muito simples e o tema acompanha perfeitamente o sistema. Neste artigo, vou compartilhar os problemas que enfrentei e a solução que encontrei.
Por que escolhi next-themes
No começo, também fiquei em dúvida se deveria escrever minha própria lógica de troca de tema. Afinal, parecia simples: bastava ler o localStorage e alterar uma classe. Na prática, porém, percebi que a renderização no servidor do Next.js torna essa tarefa bem mais complexa.
Testei algumas opções:
Implementação própria: o maior problema era o flash. Durante a SSR, o servidor não conhece a preferência de tema do usuário e renderiza o tema claro padrão. O localStorage só pode ser lido durante a hydration no cliente; quando a página muda para o tema escuro, o flash fica bem visível.
use-dark-mode: a biblioteca é boa, mas não foi projetada especificamente para Next.js e ainda apresenta alguns problemas de compatibilidade em cenários com SSR.
theme-ui: oferece muitos recursos, mas é pesada demais para um caso de uso que só exige a troca para o modo escuro. O bundle também fica maior.
Foi então que encontrei next-themes: são mais de 6.000 estrelas no GitHub, zero dependências, menos de 1 KB compactado com gzip e uma arquitetura criada especificamente para Next.js. Mais importante: ela realmente elimina o flash, já oferece suporte ao tema do sistema e persiste a preferência automaticamente. O suporte a TypeScript também é excelente, o que deixa o uso bastante agradável.
Implementação completa, passo a passo
Instale a dependência
Como sempre, comece instalando o pacote:
npm install next-themes
Se você usa pnpm ou yarn, também funciona:
pnpm add next-themes
# ou
yarn add next-themes
Crie o componente ThemeProvider
Agora precisamos criar um componente Provider. Normalmente, crio uma pasta providers ou components no projeto para esse tipo de arquivo.
Crie providers/theme-provider.tsx:
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
É obrigatório adicionar 'use client', pois next-themes precisa acessar APIs do navegador. Esse foi o primeiro problema em que tropecei: omiti a diretiva no início e recebi vários erros de hydration.
Integre ao Layout
Agora adicione o ThemeProvider ao layout raiz. Se você usa o App Router (Next.js 13+), o arquivo deve ser app/layout.tsx:
import { ThemeProvider } from '@/providers/theme-provider'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pt-BR" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Há algumas configurações importantes aqui:
attribute="class": informa ao next-themes que o tema deve ser trocado alterando a classe do elemento <html>. Isso funciona muito bem com o prefixo dark: do Tailwind CSS.
defaultTheme="system": usa o tema do sistema como padrão. Na primeira visita, a preferência de tema do sistema operacional é detectada automaticamente.
enableSystem: ativa a detecção do tema do sistema. Essa opção precisa estar habilitada; caso contrário, defaultTheme="system" não terá efeito.
disableTransitionOnChange: desativa as animações de transição durante a troca. Você pode ajustar essa opção conforme suas necessidades, mas recomendo ativá-la. Quando há transições durante a mudança para o modo escuro, todos os elementos se animam juntos, e o resultado visual costuma ficar pior.
suppressHydrationWarning: esta propriedade, adicionada à tag <html>, é muito importante. next-themes altera a classe do elemento html antes da hydration no cliente; sem ela, o React exibe um aviso.
Crie o botão de troca de tema
Com o Provider pronto, já podemos criar o botão. Crie components/theme-toggle.tsx:
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
aria-label="Trocar tema"
>
{theme === 'dark' ? '🌞' : '🌙'}
</button>
)
}
Há um pequeno truque aqui: retornar null até o componente terminar de carregar. Por quê? O tema não está disponível durante a renderização no servidor; renderizar o botão diretamente causaria uma incompatibilidade de hydration. Só depois que o componente é montado no cliente o useTheme consegue retornar o tema atual corretamente.
Se você quiser uma alternância entre três estados — light, dark e system — pode escrever assim:
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
const cycleTheme = () => {
if (theme === 'light') setTheme('dark')
else if (theme === 'dark') setTheme('system')
else setTheme('light')
}
const getIcon = () => {
if (theme === 'light') return '🌞'
if (theme === 'dark') return '🌙'
return '💻'
}
return (
<button
onClick={cycleTheme}
className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
>
{getIcon()}
</button>
)
}
Entendendo a fundo o problema do flash
O que me levou a estudar esse assunto foi justamente aquele flash irritante. Levei algum tempo para entender o mecanismo por completo.
Como o FOUC acontece
O FOUC (Flash of Unstyled Content, ou exibição momentânea de conteúdo sem estilo) é muito comum em implementações de modo escuro no Next.js. A causa está na diferença entre o estado da SSR e o estado do cliente.
Durante a renderização no servidor, o ambiente Node.js não tem o objeto window, não consegue acessar o localStorage e tampouco conhece a preferência de tema do sistema do usuário. Por isso, o servidor só pode renderizar um tema padrão, normalmente o claro.
Em seguida, o HTML chega ao navegador e começa a hydration, processo no qual o React transforma o HTML estático do servidor em componentes interativos. Só então o JavaScript consegue ler o localStorage, descobrir que o usuário havia escolhido o tema escuro e alterar o DOM para adicionar a classe dark.
Essa alteração provoca uma nova renderização, e todos os estilos precisam mudar do tema claro para o escuro. É assim que o flash aparece.
Como next-themes resolve o problema
A solução do next-themes é engenhosa: a biblioteca injeta um blocking script no <head>. Esse script é executado antes da renderização da página, lê imediatamente o tema salvo no localStorage e adiciona a classe correspondente ao elemento <html>.
De forma simplificada, a lógica se parece com isto:
(function() {
try {
const theme = localStorage.getItem('theme')
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const currentTheme = theme || systemTheme
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark')
}
} catch (e) {}
})()
Como o script é síncrono e bloqueia a renderização, a classe correta já está definida antes que qualquer conteúdo seja exibido. Assim, o CSS aplica o estilo certo desde o início e não há flash.
Erros comuns de configuração
Já vi muitas configurações problemáticas, geralmente por causa destes pontos:
Esquecer suppressHydrationWarning:
Se você não adicionar essa propriedade à tag <html>, o console exibirá continuamente este aviso:
Warning: Prop `className` did not match. Server: "" Client: "dark"
Isso não impede o funcionamento, mas incomoda.
ThemeProvider no lugar errado:
Colocar o ThemeProvider dentro de um Server Component ou fora de body pode causar problemas. Lembre-se: ele precisa envolver o conteúdo da página e ser um Client Component.
Configuração incorreta do Tailwind:
Se o seu tailwind.config.js estiver assim:
module.exports = {
darkMode: 'media',
}
Você terá um problema. O modo media é uma solução exclusivamente em CSS: ele só acompanha o tema do sistema e não permite troca manual. Altere para:
module.exports = {
darkMode: 'class',
}
Persistência do tema e acompanhamento do sistema
Como funciona a persistência
Por padrão, next-themes salva a escolha de tema no localStorage usando a chave 'theme'. Isso acontece automaticamente, sem nenhum código adicional.
Se você quiser personalizar a chave de armazenamento, configure assim:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
storageKey="my-theme"
>
{children}
</ThemeProvider>
Em alguns cenários, talvez você precise usar um cookie em vez do localStorage. Por exemplo, se quiser que o servidor conheça a preferência de tema do usuário e elimine qualquer possibilidade de flash. Nesse caso, você pode:
- Ler o cookie no middleware e adicioná-lo ao cabeçalho da resposta
- Renderizar o tema correspondente no servidor com base nesse cabeçalho
- Sincronizar o cookie com o localStorage no cliente
Mas, sinceramente, a solução padrão do next-themes já é suficiente para a maioria dos casos.
Acompanhamento do tema do sistema
A opção enableSystem permite que next-themes acompanhe as mudanças de tema do sistema. Quando o usuário alterna entre os modos claro e escuro nas configurações do sistema operacional, o aplicativo também muda automaticamente se o tema atual estiver definido como system.
Por baixo dos panos, a biblioteca monitora a media query prefers-color-scheme:
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
// Lógica de troca de tema
})
O usuário também pode substituir manualmente a preferência do sistema. Por exemplo, mesmo com o sistema no modo claro, ele pode selecionar o tema escuro no seu site. next-themes memoriza essa escolha e mantém o tema escuro na próxima visita.
Suporte a vários temas
Embora o foco aqui seja o modo escuro, next-themes aceita qualquer quantidade de temas. Você pode criar, por exemplo, temas roxo e verde:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
themes={['light', 'dark', 'purple', 'green']}
>
{children}
</ThemeProvider>
Depois, defina os estilos correspondentes no CSS:
.purple {
--background: #f3e8ff;
--foreground: #581c87;
}
.green {
--background: #dcfce7;
--foreground: #14532d;
}
Essa abordagem fica muito flexível quando combinada com variáveis CSS.
Dicas práticas e problemas comuns
Uso com Tailwind CSS
Se você usa Tailwind, a configuração fica ainda mais simples. Primeiro, confira se tailwind.config.js contém:
module.exports = {
darkMode: 'class',
// Outras configurações...
}
Depois, use o prefixo dark: normalmente:
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<h1 className="text-2xl font-bold">Título</h1>
<p className="text-gray-600 dark:text-gray-400">Texto do parágrafo</p>
</div>
A variante dark: do Tailwind entra em vigor quando o elemento <html> tem a classe dark, exatamente como next-themes funciona.
Animações e transições
Pessoalmente, recomendo ativar disableTransitionOnChange. Se o seu CSS contém muitas propriedades transition, todos os elementos serão animados ao mesmo tempo durante a troca de tema, e o resultado pode ficar confuso.
Mas, se você realmente quiser um efeito de transição, use:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange={false}
>
{children}
</ThemeProvider>
E adicione ao CSS global:
* {
transition: background-color 0.2s ease, color 0.2s ease;
}
Isso cria um efeito suave de fade durante a troca. Mesmo assim, depois de testar algumas vezes, continuo achando que a interface fica mais limpa e direta sem transição.
Suporte de tipos no TypeScript
O suporte a TypeScript do next-themes é muito bom. Para estender o tipo dos temas, você pode fazer assim:
import { useTheme } from 'next-themes'
type Theme = 'light' | 'dark' | 'purple'
export function useCustomTheme() {
const { theme, setTheme } = useTheme()
return {
theme: theme as Theme,
setTheme: (theme: Theme) => setTheme(theme),
}
}
Assim, o editor oferece sugestões de tipo e impede que você defina por engano um tema inexistente.
Solução de problemas comuns
Problema 1: o tema muda, mas os estilos não
Confira estes pontos:
- O
darkModedo Tailwind está definido como'class'? - O CSS usa corretamente o prefixo
dark:ou o seletor.dark? - A classe foi adicionada corretamente ao elemento
<html>? Confira no console do navegador.
Problema 2: a página ainda pisca ao atualizar
Se ainda houver flash, as causas possíveis são:
- Você esqueceu de adicionar
suppressHydrationWarningao elemento<html> - O ThemeProvider está no lugar errado
- Outro script está interferindo, como o Google Analytics
Problema 3: o tema não acompanha o sistema
Confira:
enableSystemestá definido comotrue?- O navegador aceita
prefers-color-scheme? Todos os navegadores modernos aceitam. - O tema atual está definido como
system? Se houve uma troca manual, ele pode estar comolightoudark.
Conclusão
Relembrando o caminho desde a frustração inicial com o flash até uma implementação fluida de modo escuro, next-themes foi uma grande ajuda. A biblioteca não resolve apenas um problema técnico: ela também melhora a experiência do usuário.
Vamos recapitular os pontos principais:
next-themesresolve o flash do modo escuro no Next.js praticamente sem configuração- Adicione
suppressHydrationWarningao elemento<html>e marque o ThemeProvider como Client Component - Defina
darkModecomo'class'no Tailwind - Renderize o botão de troca de tema somente depois de mounted para evitar incompatibilidade de hydration
- O acompanhamento do tema do sistema e a troca manual podem coexistir perfeitamente
Se você ainda não experimentou next-themes em um projeto, vale muito a pena testar. A documentação oficial também é bem clara: github.com/pacocoursey/next-themes
Agora é só adicionar um modo escuro fluido ao seu projeto Next.js. Seus usuários vão agradecer!
Processo completo para implementar modo escuro no Next.js
Use next-themes para criar um modo escuro sem flashes, com acompanhamento do tema do sistema e troca manual
⏱️ Estimated time: 30 min
- 1
Step 1: Instale next-themes
Instale a dependência:
• npm install next-themes
Ou use outro gerenciador de pacotes:
• pnpm add next-themes
• yarn add next-themes
Observação: next-themes não tem dependências e ocupa muito pouco espaço - 2
Step 2: Configure o ThemeProvider
Adicione-o ao layout raiz:
• Crie o arquivo providers.tsx (marcado com 'use client')
• Envolva children com ThemeProvider
• Importe e use o componente em app/layout.tsx
Configurações principais:
• attribute="class": usa uma classe para trocar o tema
• enableSystem: ativa o acompanhamento do tema do sistema
• storageKey: nome da chave no localStorage
Observação: ThemeProvider precisa ser um Client Component - 3
Step 3: Configure o Tailwind CSS
Em tailwind.config.js:
• Defina darkMode: 'class'
• Assim, o Tailwind troca o tema conforme a classe da tag html
Exemplo de configuração:
module.exports = {
darkMode: 'class',
// ... outras configurações
}
Use o prefixo dark: para definir estilos escuros:
className="bg-white dark:bg-gray-900" - 4
Step 4: Corrija o aviso de hydration
Adicione à tag html:
• A propriedade suppressHydrationWarning
• Ela evita o aviso causado pela diferença de tema entre servidor e cliente
Em layout.tsx:
<html lang="pt-BR" suppressHydrationWarning>
<body>{children}</body>
</html>
Isso evita o aviso de hydration do Next.js - 5
Step 5: Crie o botão de troca de tema
Use o hook useTheme:
• Crie o componente ThemeToggle (marcado com 'use client')
• Use useTheme() para obter theme e setTheme
• Espere mounted antes de renderizar para evitar incompatibilidade de hydration
Exemplo:
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Trocar tema
</button> - 6
Step 6: Teste e valide
Pontos a testar:
• Teste a troca manual de tema (sem flashes)
• Teste o acompanhamento do tema do sistema
• Teste se o tema permanece após atualizar a página
• Teste a consistência do tema entre páginas
Checklist:
• A página carrega sem flashes
• A troca de tema é fluida
• O localStorage armazena a opção corretamente
• A troca do tema do sistema é acompanhada automaticamente
FAQ
Por que a página pisca durante o carregamento?
Qual é a diferença entre next-themes e outras bibliotecas de temas?
Como acompanhar o tema do sistema?
Por que suppressHydrationWarning é necessário?
Por que o botão de troca de tema só deve ser renderizado depois de mounted?
Como personalizar a lógica de troca de tema?
Quais temas next-themes aceita?
11 min de leitura · Publicado em: 20 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Guia completo de monitoramento em produção para Next.js: Sentry, logs e alertas
Aprenda a montar um sistema de monitoramento em produção para Next.js com Sentry, logs estruturados, monitoramento de desempenho e alertas, incluindo App Router e modelos de código prontos.
Parte 43 de 51
Próximo
Boas práticas de Next.js com Tailwind CSS: da configuração ao modo escuro (2025)
Guia prático de Next.js com Tailwind CSS v4 em 2025 para reduzir classes extensas, configurar temas personalizados, implementar o modo escuro e otimizar o CSS de 500 KB para 50 KB.
Parte 45 de 51



Comentários
Entre com GitHub para comentar