Prompt-to-PR: Añadir Modo Oscuro
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.
CursorClaude CodeCodexWindsurf Next.jsTypeScriptTailwind
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.
1. Requisito
Añ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:
- Sin parpadeo en la carga inicial (sin destello blanco en modo oscuro).
- Persistido en
localStorage. - Por defecto, la preferencia del sistema (
system). - Alternable mediante un botón en el encabezado del sitio.
2. Primer 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. Cambios de Archivos Esperados
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 Verificación
tailwind.config.tstienedarkMode: "class"— no"media"(media no permite alternancia).ThemeProvideres un Componente Cliente ("use client") que envuelvechildrendel layout raíz del Servidor.- La etiqueta
<html>o<body>NO tiene unclass="dark"hardcodeado —next-themeslo añade dinámicamente. ThemeTogglerenderizanull(o un placeholder) hasta quemounted === truepara evitar discrepancias de hidratación.- No falta
suppressHydrationWarningen<html>—next-themeslo requiere; confirmar que está presente en la etiqueta<html>. - Las variantes
dark:aparecen solo en contenedores a nivel de layout, no en cada elemento. - La clave de
localStorageusada pornext-themeses la predeterminadatheme— sin clave personalizada que entre en conflicto con otro almacenamiento.
5. Comandos de Prueba
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 --noEmitbun run build# Confirm no hydration warnings in the browser console after bun run start6. Fallos Comunes
- Destello blanco en carga (FOIT) —
ThemeProviderno envuelve el layout raíz, o faltasuppressHydrationWarningen<html>.next-themesinyecta un script en<head>para establecer la clase antes del pintado — esto solo funciona cuando envuelve el elemento más externo. - Error de discrepancia de hidratación —
ThemeTogglerenderiza contenido específico del tema (icono) antes demounted. Añadirconst [mounted, setMounted] = useState(false)yuseEffect(() => setMounted(true), []). Devolver un placeholder hasta que esté montado. - Clases
dark:no aplicadas —tailwind.config.tsaún tienedarkMode: "media"o falta la clave. La estrategia de clase requiere el valor"class". - 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.
7. Prompt de Corrección
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 andsets the class on <html> synchronously before paint. This only works if theThemeProvider is rendered above the content it styles.8. Descripción del PR
## 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