P PasteCode
Playbook

Prompt-to-PR: Adicionar Modo Escuro

SOP completa para adicionar modo escuro sem cintilação a um aplicativo Next.js Tailwind usando next-themes — estratégia de classe, preferência do sistema e alternância persistida.

CursorClaude CodeCodexWindsurf Next.jsTypeScriptTailwind
.md .json Dificuldade: Fácil Atualizado 8 de jun. de 2026

Modo escuro feito de forma errada causa um flash de tema incorreto (FOIT) a cada carregamento de página. Este guia produz uma implementação sem cintilação usando next-themes com a estratégia class do Tailwind.

1. Requisito

Adicione uma alternância persistente de modo escuro/claro/sistema a um site Next.js 15 App Router usando Tailwind. O tema deve ser:

  • Sem cintilação no carregamento inicial (sem flash branco no modo escuro).
  • Persistido em localStorage.
  • Padrão para a preferência do SO (system).
  • Alternável através de um botão no cabeçalho do site.

2. Primeiro Prompt

First Prompt
Add flicker-free dark mode to this Next.js 15 App Router project with Tailwind.
Requirements:
1. Install `next-themes`.
2. In `tailwind.config.ts`, set `darkMode: "class"`.
3. Create `src/components/ThemeProvider.tsx` — a thin Client Component wrapper
around `next-themes` ThemeProvider:
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
Mark it "use client".
4. Wrap the root layout's `<body>` with ThemeProvider in
`src/app/layout.tsx`. Keep the body tag; just wrap children.
5. Create `src/components/ThemeToggle.tsx` — a Client Component with a button
that cycles: light -> dark -> system. Use `useTheme()` from next-themes.
Show the current theme as an icon or label. Handle the mounted check to
avoid hydration mismatch (render null or a skeleton until mounted).
6. Add `<ThemeToggle />` to the existing site header/nav component.
7. Do not change any color values or CSS — only add `dark:` Tailwind variants
where the existing design clearly has background and text colors that need
inversion. Update: bg-white -> bg-white dark:bg-gray-950, text-gray-900
-> text-gray-900 dark:text-gray-100.
Do not invert every element — only top-level layout containers.

3. Alterações Esperadas nos Arquivos

package.json (next-themes)
tailwind.config.ts (darkMode: "class")
src/app/layout.tsx (ThemeProvider wrapper)
src/components/ThemeProvider.tsx (new — "use client" wrapper)
src/components/ThemeToggle.tsx (new — theme cycling button)
src/components/Header.tsx (add ThemeToggle — or whichever nav file)

4. Lista de Verificação

  • tailwind.config.ts tem darkMode: "class" — não "media" (media não permite alternância).
  • ThemeProvider é um Componente Cliente ("use client") envolvendo children do layout raiz do Componente Servidor.
  • O <html> ou <body> raiz NÃO tem um class="dark" fixo — next-themes adiciona isso dinamicamente.
  • ThemeToggle renderiza null (ou um placeholder) até mounted === true para evitar incompatibilidade de hidratação.
  • O atributo suppressHydrationWarning no <html> não está faltando — next-themes exige isso; confirme que está presente na tag <html>.
  • As variantes dark: aparecem apenas em contêineres de nível de layout, não em cada elemento individualmente.
  • A chave localStorage usada por next-themes é a padrão theme — nenhuma chave personalizada que conflite com outro armazenamento.

5. Comandos de Teste

Terminal window
bun dev
# In Chrome DevTools: set prefers-color-scheme to dark
# Reload — confirm no white flash before dark theme applies
# Toggle the button — confirm light/dark/system cycle
# Reload after each — confirm preference persists
bun tsc --noEmit
bun run build
# Confirm no hydration warnings in the browser console after bun run start

6. Falhas Comuns

  • Flash branco no carregamento (FOIT)ThemeProvider não está envolvendo o layout raiz, ou suppressHydrationWarning está ausente no <html>. next-themes injeta um script no <head> para definir a classe antes da pintura — isso só funciona quando envolve o elemento mais externo.
  • Erro de incompatibilidade de hidrataçãoThemeToggle renderiza conteúdo específico do tema (ícone) antes de mounted. Adicione const [mounted, setMounted] = useState(false) e useEffect(() => setMounted(true), []). Retorne um placeholder até que mounted seja true.
  • Classes dark: não aplicadastailwind.config.ts ainda tem darkMode: "media" ou a chave está faltando. A estratégia de classe exige o valor "class".
  • Alternância não visível<ThemeToggle> importado em um Componente Servidor sem o limite "use client". A cadeia de importação deve passar por um Componente Cliente.

7. Prompt de Correção

Fix Prompt
There is a white flash before dark mode applies on page load.
Confirm:
1. In src/app/layout.tsx, the <html> tag has suppressHydrationWarning.
2. ThemeProvider wraps {children} directly inside <body>.
3. ThemeProvider uses attribute="class" (not attribute="data-theme").
next-themes injects an inline script into <head> that reads localStorage and
sets the class on <html> synchronously before paint. This only works if the
ThemeProvider is rendered above the content it styles.

8. Descrição do PR

PR description
## Feature: Flicker-free dark mode via next-themes
- `tailwind.config.ts`: `darkMode: "class"`
- `ThemeProvider` wraps root layout — `next-themes` sets `<html class="dark">`
synchronously before first paint (no flash)
- `ThemeToggle` cycles light / dark / system; persists to `localStorage`
- Mounted guard in ThemeToggle prevents hydration mismatch
- `dark:` variants added to top-level layout containers only