Dark mode con next-themes y Tailwind CSS v4: la solución definitiva
Cómo implementar dark/light mode en Next.js con next-themes y Tailwind CSS v4 sin flash, sin hydration mismatch y con transiciones suaves.
- #dark-mode
- #next-themes
- #tailwind-css
- #next.js
El dark mode parece simple hasta que lo implementas. Flash de tema incorrecto al cargar, hydration mismatch en SSR, colores que no transicionan bien, toggles que parpadean. He pasado por todos esos problemas y esta es la solución que funciona.
El problema del flash
En una app con SSR, el servidor no sabe qué tema prefiere el usuario. Si renderizas con el tema por defecto y el usuario tiene el opuesto, verá un flash de color al cargar. Eso es inaceptable en un producto profesional.
La solución: next-themes inyecta un script inline que lee la preferencia antes de que React hidrate. El script aplica la clase .dark o la quita inmediatamente, sin esperar a JavaScript.
Setup con next-themes
La configuración que uso en todos mis proyectos Next.js:
El ThemeProvider envuelve la app con tres props clave: attribute="class" para usar clases CSS, defaultTheme="dark" como tema por defecto, y enableSystem para respetar la preferencia del sistema operativo.
El suppressHydrationWarning en el tag es necesario porque next-themes modifica el DOM antes de la hidratación. Sin él, React lanza un warning de mismatch.
Variables CSS para los colores
En vez de usar dark:bg-gray-900 bg-white en cada elemento, defino variables CSS que cambian según el tema. En :root defino la paleta light, en .dark la dark:
Cada componente usa var(--color-background), var(--color-foreground), etc. Un cambio en el tema actualiza todas las variables simultáneamente.
Tailwind CSS v4: @custom-variant
La pieza que conecta todo con Tailwind v4: @custom-variant dark (&:is(.dark, .dark *));. Esta declaración le dice a Tailwind que las clases dark: deben activarse cuando hay una clase .dark en un ancestro.
Sin esto, dark:text-white no funciona con el approach de clase que usa next-themes.
Transiciones suaves
El error más común: cambiar el tema y que todo cambie instantáneamente. Se ve brusco, especialmente en desktop con pantallas grandes.
La solución: una transición global en CSS que aplica a background-color, color y border-color con una duración suave. Añadir estas propiedades de transición al selector * con baja especificidad permite que todos los elementos transicionen sin interferir con animaciones más específicas de hover o layout.
Los elementos con transiciones propias como botones o cards necesitan incluir background-color y color en su declaración de transition para participar en la transición de tema.
El toggle
El componente ThemeToggle usa useTheme() de next-themes para acceder al tema actual y cambiarlo. Dos detalles importantes: esperar al mount antes de renderizar el icono correcto para evitar el hydration mismatch, y añadir una animación de rotación al cambiar para dar feedback visual.
Imágenes y SVGs
Para logos y assets que cambian entre temas, el patrón más limpio: renderizar ambas versiones y alternar visibilidad con hidden dark:block y block dark:hidden. Es un truco simple que evita JavaScript extra para cambiar la fuente de una imagen.
Conclusión
Dark mode bien hecho es un detalle que marca la diferencia entre un proyecto amateur y uno profesional. Con next-themes + Tailwind CSS v4 + variables CSS, la implementación es sólida, sin flash, sin hydration warnings y con transiciones que se sienten naturales.
¿Construyendo algo?
Si esto te ha resonado, podemos hablar. Contactar →