nacs digital lab
Development

Implementando dark mode con Tailwind v4

De @apply a @theme: cómo migré el dark mode y qué aprendí en el proceso

5 min de lectura
Vista previa del video: Implementando dark mode con Tailwind v4

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

FeatureTailwind v3Tailwind v4
Config file✅ Necesario❌ Opcional
Dark mode configdarkMode: 'class'@custom-variant dark
Variableshsl(var(--color))oklch(var(--color))
Theme extendEn 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