Ver video
Tailwind v4 cambió TODO sobre cómo configuramos estilos. Y el dark mode no fue la excepción.
Si vienes de Tailwind v3, prepárate: algunas cosas que hacías antes ya no funcionan. Pero la nueva forma es más elegante.
El problema
En Tailwind v3, mi setup de dark mode era así:
// tailwind.config.js (v3)
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
}
}
}
}
/* globals.css (v3) */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
}
Esto dejó de funcionar en v4.
Qué cambió en Tailwind v4
1. Adiós al config.js
Tailwind v4 eliminó tailwind.config.js para estilos básicos. Todo va en CSS ahora:
/* La nueva forma */
@import "tailwindcss";
Sí, eso es todo. El config ya no es necesario para la mayoría de casos.
2. @theme en lugar de configuración JavaScript
En lugar de extender el theme en JS, ahora usas @theme en CSS:
@theme {
--color-background: oklch(0.98 0 0);
--color-foreground: oklch(0.09 0 0);
}
3. @custom-variant para dark mode
Aquí está la magia. En lugar de configurar darkMode: 'class' en el config, ahora defines un custom variant:
@custom-variant dark (&:where(.dark, .dark *));
Esta línea le dice a Tailwind: “cuando el elemento o su ancestro tiene la clase .dark, aplica estos estilos”.
Mi implementación final
globals.css completo
@import "tailwindcss";
/* Custom variant para dark mode */
@custom-variant dark (&:where(.dark, .dark *));
/* Variables de color */
:root {
--color-background: oklch(0.98 0 0);
--color-foreground: oklch(0.09 0 0);
--color-border: oklch(0.92 0 0);
}
.dark {
--color-background: oklch(0.15 0 0);
--color-foreground: oklch(0.98 0 0);
--color-border: oklch(0.25 0 0);
}
/* Mapear variables a Tailwind */
@theme inline {
--color-background: var(--color-background);
--color-foreground: var(--color-foreground);
--color-border: var(--color-border);
}
@layer base {
body {
background-color: var(--color-background);
color: var(--color-foreground);
}
}
ThemeToggle Component
// React component para el toggle
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
// Cargar tema guardado o del sistema
const saved = localStorage.getItem('theme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
const current = saved || system;
setTheme(current);
document.documentElement.classList.toggle('dark', current === 'dark');
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
Evitar el flash (FOUC)
El problema: hay un flash de contenido cuando la página carga antes de que JavaScript aplique el tema.
Solución: Script inline en el <head>:
<script>
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
})();
</script>
Este script se ejecuta antes del render, evitando el flash.
Errores que cometí (para que tú no los cometas)
1. Olvidé @theme inline
Al principio solo definí las variables en :root y .dark. Pero Tailwind v4 no las reconocía automáticamente.
Necesitas mapearlas explícitamente con @theme inline.
2. Usé hsl() en lugar de oklch()
Tailwind v4 recomienda oklch() para mejor soporte de colores. Es el futuro de CSS colors.
3. No puse el script inline
Sin el script en <head>, veía un flash al recargar. Usuarios con dark mode veían un segundo de tema claro. Mala UX.
Comparación v3 vs v4
| Feature | Tailwind v3 | Tailwind v4 |
|---|---|---|
| Config file | ✅ Necesario | ❌ Opcional |
| Dark mode config | darkMode: 'class' | @custom-variant dark |
| Variables | hsl(var(--color)) | oklch(var(--color)) |
| Theme extend | En JS | @theme en CSS |
| Build speed | ~2s | ~0.5s (4x más rápido) |
Resultados
Antes (v3):
- Dark mode funcionaba pero configuración en 2 archivos
- Build time: 2.1s
- Bundle size: ~8KB
Después (v4):
- Todo en CSS, más declarativo
- Build time: 0.5s
- Bundle size: ~4KB
- Mejor performance en dev mode
Casos edge que resolver
1. Imágenes con tema
Algunas imágenes se ven mal en dark mode. Solución:
.dark img {
filter: brightness(0.8) contrast(1.2);
}
2. Borders sutiles
En dark mode, borders muy claros desaparecen. Ajusta el contraste:
:root {
--color-border: oklch(0.92 0 0); /* casi blanco */
}
.dark {
--color-border: oklch(0.25 0 0); /* gris medio, no negro */
}
3. Syntax highlighting
Para code blocks, usa un tema diferente en dark mode:
// astro.config.mjs
export default defineConfig({
markdown: {
shikiConfig: {
theme: 'github-light',
themes: {
light: 'github-light',
dark: 'github-dark'
}
}
}
});
Vale la pena migrar a v4?
Si empiezas un proyecto nuevo: SÍ, sin duda.
Si tienes un proyecto en v3: Depende.
- ¿Usas muchos @apply customs? La migración será más compleja.
- ¿Solo usas utilidades? La migración es rápida (2-3 horas).
Para mi sitio personal, migré en 1 hora y el resultado vale la pena.
Recursos
Consejo final
No migres por migrar. Tailwind v3 sigue siendo excelente.
Pero si buscas mejor performance, código más limpio, y disfrutas estar en el bleeding edge… v4 es una actualización increíble.
El dark mode es solo más elegante ahora. Y eso me gusta.
Contenido Relacionado
Más experimentos de Development