{
  "id": "add-dark-mode",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "es",
  "url": "/es/playbooks/add-dark-mode",
  "title": "Prompt-to-PR: Añadir Modo Oscuro",
  "description": "SOP completo para añadir modo oscuro sin parpadeo a una aplicación Next.js Tailwind usando next-themes — estrategia de clase, preferencia del sistema y alternancia persistente.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "tailwind",
    "typescript"
  ],
  "difficulty": "easy",
  "updated": "2026-06-08",
  "markdown": "El modo oscuro mal implementado provoca un destello de tema incorrecto (FOIT) en cada carga de página. Este manual produce una implementación sin parpadeo usando `next-themes` con la estrategia de clase de Tailwind.\n\n## 1. Requisito\n\nAñadir un botón de alternancia de modo oscuro/claro/sistema persistente a un sitio Next.js 15 con App Router y Tailwind. El tema debe ser:\n- Sin parpadeo en la carga inicial (sin destello blanco en modo oscuro).\n- Persistido en `localStorage`.\n- Por defecto, la preferencia del sistema (`system`).\n- Alternable mediante un botón en el encabezado del sitio.\n\n## 2. Primer 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. Cambios de Archivos Esperados\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 Verificación\n\n- `tailwind.config.ts` tiene `darkMode: \"class\"` — no `\"media\"` (media no permite alternancia).\n- `ThemeProvider` es un Componente Cliente (`\"use client\"`) que envuelve `children` del layout raíz del Servidor.\n- La etiqueta `<html>` o `<body>` NO tiene un `class=\"dark\"` hardcodeado — `next-themes` lo añade dinámicamente.\n- `ThemeToggle` renderiza `null` (o un placeholder) hasta que `mounted === true` para evitar discrepancias de hidratación.\n- No falta `suppressHydrationWarning` en `<html>` — `next-themes` lo requiere; confirmar que está presente en la etiqueta `<html>`.\n- Las variantes `dark:` aparecen solo en contenedores a nivel de layout, no en cada elemento.\n- La clave de `localStorage` usada por `next-themes` es la predeterminada `theme` — sin clave personalizada que entre en conflicto con otro almacenamiento.\n\n## 5. Comandos de Prueba\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. Fallos Comunes\n\n- **Destello blanco en carga (FOIT)** — `ThemeProvider` no envuelve el layout raíz, o falta `suppressHydrationWarning` en `<html>`. `next-themes` inyecta un script en `<head>` para establecer la clase antes del pintado — esto solo funciona cuando envuelve el elemento más externo.\n- **Error de discrepancia de hidratación** — `ThemeToggle` renderiza contenido específico del tema (icono) antes de `mounted`. Añadir `const [mounted, setMounted] = useState(false)` y `useEffect(() => setMounted(true), [])`. Devolver un placeholder hasta que esté montado.\n- **Clases `dark:` no aplicadas** — `tailwind.config.ts` aún tiene `darkMode: \"media\"` o falta la clave. La estrategia de clase requiere el valor `\"class\"`.\n- **Alternancia no visible** — `<ThemeToggle>` importado en un Componente Servidor sin el límite `\"use client\"`. La cadena de importación debe pasar por un Componente Cliente.\n\n## 7. Prompt de Corrección\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. Descripción del 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```"
}