{
  "id": "add-dark-mode",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "pt",
  "url": "/pt/playbooks/add-dark-mode",
  "title": "Prompt-to-PR: Adicionar Modo Escuro",
  "description": "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.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "tailwind",
    "typescript"
  ],
  "difficulty": "easy",
  "updated": "2026-06-08",
  "markdown": "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.\n\n## 1. Requisito\n\nAdicione uma alternância persistente de modo escuro/claro/sistema a um site Next.js 15 App Router usando Tailwind. O tema deve ser:\n- Sem cintilação no carregamento inicial (sem flash branco no modo escuro).\n- Persistido em `localStorage`.\n- Padrão para a preferência do SO (`system`).\n- Alternável através de um botão no cabeçalho do site.\n\n## 2. Primeiro Prompt\n\n```txt title=\"First Prompt\"\nAdd flicker-free dark mode to this Next.js 15 App Router project with Tailwind.\n\nRequirements:\n1. Install `next-themes`.\n2. In `tailwind.config.ts`, set `darkMode: \"class\"`.\n3. Create `src/components/ThemeProvider.tsx` — a thin Client Component wrapper\n   around `next-themes` ThemeProvider:\n     <ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n       {children}\n     </ThemeProvider>\n   Mark it \"use client\".\n4. Wrap the root layout's `<body>` with ThemeProvider in\n   `src/app/layout.tsx`. Keep the body tag; just wrap children.\n5. Create `src/components/ThemeToggle.tsx` — a Client Component with a button\n   that cycles: light -> dark -> system. Use `useTheme()` from next-themes.\n   Show the current theme as an icon or label. Handle the mounted check to\n   avoid hydration mismatch (render null or a skeleton until mounted).\n6. Add `<ThemeToggle />` to the existing site header/nav component.\n7. Do not change any color values or CSS — only add `dark:` Tailwind variants\n   where the existing design clearly has background and text colors that need\n   inversion. Update: bg-white -> bg-white dark:bg-gray-950, text-gray-900\n   -> text-gray-900 dark:text-gray-100.\n   Do not invert every element — only top-level layout containers.\n```\n\n## 3. Alterações Esperadas nos Arquivos\n\n```txt\npackage.json                          (next-themes)\ntailwind.config.ts                    (darkMode: \"class\")\nsrc/app/layout.tsx                    (ThemeProvider wrapper)\nsrc/components/ThemeProvider.tsx      (new — \"use client\" wrapper)\nsrc/components/ThemeToggle.tsx        (new — theme cycling button)\nsrc/components/Header.tsx             (add ThemeToggle — or whichever nav file)\n```\n\n## 4. Lista de Verificação\n\n- `tailwind.config.ts` tem `darkMode: \"class\"` — não `\"media\"` (media não permite alternância).\n- `ThemeProvider` é um Componente Cliente (`\"use client\"`) envolvendo `children` do layout raiz do Componente Servidor.\n- O `<html>` ou `<body>` raiz NÃO tem um `class=\"dark\"` fixo — `next-themes` adiciona isso dinamicamente.\n- `ThemeToggle` renderiza `null` (ou um placeholder) até `mounted === true` para evitar incompatibilidade de hidratação.\n- O atributo `suppressHydrationWarning` no `<html>` não está faltando — `next-themes` exige isso; confirme que está presente na tag `<html>`.\n- As variantes `dark:` aparecem apenas em contêineres de nível de layout, não em cada elemento individualmente.\n- A chave `localStorage` usada por `next-themes` é a padrão `theme` — nenhuma chave personalizada que conflite com outro armazenamento.\n\n## 5. Comandos de Teste\n\n```bash\nbun dev\n\n# In Chrome DevTools: set prefers-color-scheme to dark\n# Reload — confirm no white flash before dark theme applies\n\n# Toggle the button — confirm light/dark/system cycle\n# Reload after each — confirm preference persists\n\nbun tsc --noEmit\nbun run build\n# Confirm no hydration warnings in the browser console after bun run start\n```\n\n## 6. Falhas Comuns\n\n- **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.\n- **Erro de incompatibilidade de hidratação** — `ThemeToggle` 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.\n- **Classes `dark:` não aplicadas** — `tailwind.config.ts` ainda tem `darkMode: \"media\"` ou a chave está faltando. A estratégia de classe exige o valor `\"class\"`.\n- **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.\n\n## 7. Prompt de Correção\n\n```txt title=\"Fix Prompt\"\nThere is a white flash before dark mode applies on page load.\n\nConfirm:\n1. In src/app/layout.tsx, the <html> tag has suppressHydrationWarning.\n2. ThemeProvider wraps {children} directly inside <body>.\n3. ThemeProvider uses attribute=\"class\" (not attribute=\"data-theme\").\n\nnext-themes injects an inline script into <head> that reads localStorage and\nsets the class on <html> synchronously before paint. This only works if the\nThemeProvider is rendered above the content it styles.\n```\n\n## 8. Descrição do PR\n\n```md title=\"PR description\"\n## Feature: Flicker-free dark mode via next-themes\n\n- `tailwind.config.ts`: `darkMode: \"class\"`\n- `ThemeProvider` wraps root layout — `next-themes` sets `<html class=\"dark\">`\n  synchronously before first paint (no flash)\n- `ThemeToggle` cycles light / dark / system; persists to `localStorage`\n- Mounted guard in ThemeToggle prevents hydration mismatch\n- `dark:` variants added to top-level layout containers only\n```"
}