Modo oscuro
Modo oscuro, esquemas de color y tematización dinámica con Tailwind CSS v4.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
// Utilidades de modo oscuro — solo añade el prefijo dark:
<div className="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<p className="text-gray-600 dark:text-gray-400">Se adapta a la preferencia del sistema</p>
<button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400">
Haz clic en mí
</button>
</div>/* globals.css — Tailwind v4 usa prefers-color-scheme por defecto */
@import "tailwindcss";
/* Para modo oscuro basado en clase (alternado con JS): */
@variant dark (&:where(.dark, .dark *));
/* Tokens semánticos que cambian automáticamente */
@theme {
--color-surface: #ffffff;
--color-on-surface: #111827;
}
@layer base {
.dark {
--color-surface: #0f172a;
--color-on-surface: #f1f5f9;
}
}Cuándo usarlo: Toda aplicación orientada al usuario debería admitir modo oscuro: reduce la fatiga visual y respeta las preferencias del usuario.
Ejemplo funcional
"use client";
import { useEffect, useState } from "react";
type Theme = "light" | "dark" | "system";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("system");
useEffect(() => {
const stored = localStorage.getItem("theme") as Theme | null;
if (stored) setTheme(stored);
}, []);
useEffect(() => {
const root = document.documentElement;
if (theme === "system") {
root.classList.remove("dark");
const mq = window.matchMedia("(prefers-color-scheme: dark)");
if (mq.matches) root.classList.add("dark");
const handler = (e: MediaQueryListEvent) => {
root.classList.toggle("dark", e.matches);
};
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}
root.classList.toggle("dark", theme === "dark");
localStorage.setItem("theme", theme);
}, [theme]);
return (
<div className="min-h-screen bg-surface text-on-surface transition-colors duration-300">
<header className="flex items-center justify-between border-b border-gray-200 p-4 dark:border-gray-800">
<h1 className="text-lg font-bold">Mi app</h1>
<ThemeToggle theme={theme} setTheme={setTheme} />
</header>
<main className="mx-auto max-w-4xl p-6">{children}</main>
</div>
);
}
function ThemeToggle({
theme,
setTheme,
}: {
theme: Theme;
setTheme: (t: Theme) => void;
}) {
const options: { value: Theme; label: string }[] = [
{ value: "light", label: "Claro" },
{ value: "dark", label: "Oscuro" },
{ value: "system", label: "Sistema" },
];
return (
<div className="flex gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800">
{options.map((opt) => (
<button
key={opt.value}
onClick={() => setTheme(opt.value)}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
theme === opt.value
? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
}`}
>
{opt.label}
</button>
))}
</div>
);
}/* globals.css para el ejemplo anterior */
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
@theme {
--color-surface: #ffffff;
--color-on-surface: #111827;
--color-surface-muted: #f9fafb;
--color-primary: #2563eb;
--color-primary-foreground: #ffffff;
}
@layer base {
.dark {
--color-surface: #0f172a;
--color-on-surface: #f1f5f9;
--color-surface-muted: #1e293b;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
}
}Lo que esto demuestra:
- Alternancia de modo oscuro basado en clase con respaldo del sistema
- Tokens de color semánticos mediante propiedades personalizadas de CSS
- Persistencia del tema en
localStorage - Detección de preferencia del sistema con
matchMedia - Transiciones de color suaves con
transition-colors
Análisis profundo
Cómo funciona
- Por defecto,
dark:de Tailwind v4 usa@media (prefers-color-scheme: dark)— no hace falta configuración - Para alternancia basada en clase, sobrescribe con
@variant dark (&:where(.dark, .dark *))en tu CSS - El selector
:where()mantiene la especificidad en cero, así quedark:bg-gray-900no supera abg-whitepor especificidad - Los tokens semánticos (propiedades personalizadas de CSS) te permiten definir colores una vez e intercambiarlos en modo oscuro
next-themeses la biblioteca estándar para modo oscuro en Next.js: gestiona SSR, prevención de flash y sincronización con el sistema
Variaciones
Usando next-themes:
// app/layout.tsx
import { ThemeProvider } from "next-themes";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
);
}
// Uso en componentes
"use client";
import { useTheme } from "next-themes";
function Toggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Alternar
</button>
);
}Múltiples temas (más allá de claro/oscuro):
@layer base {
[data-theme="ocean"] {
--color-primary: #0891b2;
--color-surface: #f0fdfa;
}
[data-theme="sunset"] {
--color-primary: #ea580c;
--color-surface: #fff7ed;
}
}Imágenes conscientes del modo oscuro:
<picture>
<source srcSet="/logo-dark.svg" media="(prefers-color-scheme: dark)" />
<img src="/logo-light.svg" alt="Logo" />
</picture>
{/* O con Tailwind: */}
<img src="/logo-light.svg" className="dark:hidden" alt="Logo" />
<img src="/logo-dark.svg" className="hidden dark:block" alt="Logo" />Notas de TypeScript
// Tipo Theme
type Theme = "light" | "dark" | "system";
// Tipos de next-themes
import type { ThemeProviderProps } from "next-themes";
// Colores de tema con tipado seguro
const themeColors = {
light: { bg: "bg-white", text: "text-gray-900" },
dark: { bg: "bg-gray-950", text: "text-gray-100" },
} as const satisfies Record<string, { bg: string; text: string }>;Trampas
-
Flash del tema incorrecto (FOIT) — Sin gestión de SSR, la página muestra brevemente el tema equivocado. Solución: Usa
next-themesconsuppressHydrationWarningen<html>, o añade un<script>bloqueante en<head>que establezca la clase. -
prefers-color-schemefrente a alternancia por clase — El comportamiento por defecto de v4 usa la media query. Si añades alternancia con la clase.dark, debes sobrescribir la variante. Solución: Añade@variant dark (&:where(.dark, .dark *)). -
Modificadores de opacidad en modo oscuro —
bg-blue-500/50funciona, perodark:bg-blue-500/50puede no componerse bien con tokens semánticos. Solución: Define valores de opacidad separados en tus tokens de tema. -
Contraste de color — El modo oscuro no consiste en «invertir todo». El texto claro sobre fondos oscuros necesita ratios de contraste distintos. Solución: Prueba con verificadores de contraste WCAG.
-
Imágenes y sombras — Las imágenes brillantes y las sombras oscuras se ven mal en modo oscuro. Solución: Reduce la intensidad de las sombras (
dark:shadow-none), añade superposiciones en las imágenes o usa el filtrobrightness.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
next-themes | App Next.js con SSR (evita el flash) | Proyectos que no usan Next.js |
CSS color-scheme: dark | Quieres que la UI del navegador (scrollbars, inputs) se adapte | Necesitas tematización personalizada de componentes |
Solo prefers-color-scheme | Quieres modo oscuro automático según el sistema | Los usuarios necesitan control manual de alternancia |
Tailwind darkMode: "selector" (v3) | Estás en Tailwind v3 | Estás en v4 |
FAQs
¿Cuál es la estrategia de modo oscuro por defecto en Tailwind v4?
Por defecto, dark: usa @media (prefers-color-scheme: dark) — sigue la preferencia del sistema operativo del usuario sin necesidad de configuración.
¿Cómo cambias del modo oscuro por media query a alternancia basada en clase?
Añade esto a tu CSS:
@variant dark (&:where(.dark, .dark *));Luego alterna la clase .dark en el elemento <html> mediante JavaScript.
¿Qué son los tokens de color semánticos y por qué usarlos?
- Propiedades personalizadas de CSS como
--color-surfacey--color-on-surface - Definidos una vez en
:root, sobrescritos en.dark - Los componentes usan
bg-surfaceen lugar debg-white dark:bg-gray-950 - Reduce la duplicación y facilita los cambios de tema
¿Cuál es la biblioteca recomendada para modo oscuro en Next.js?
next-themes. Gestiona SSR, evita el flash del tema incorrecto, admite la preferencia del sistema y funciona con la estrategia del atributo class.
¿Por qué hace falta suppressHydrationWarning en la etiqueta <html>?
next-themes añade el atributo class en el cliente antes de la hidratación. Sin suppressHydrationWarning, React advierte sobre el desajuste servidor/cliente.
¿Cómo admitir más de dos temas (p. ej., ocean, sunset)?
@layer base {
[data-theme="ocean"] {
--color-primary: #0891b2;
}
[data-theme="sunset"] {
--color-primary: #ea580c;
}
}Usa un atributo data-theme en lugar de la clase .dark.
¿Cómo mostrar imágenes distintas para modo claro y oscuro?
<img src="/logo-light.svg" className="dark:hidden" alt="Logo" />
<img src="/logo-dark.svg" className="hidden dark:block" alt="Logo" />Trampa: La página muestra un flash del tema incorrecto al cargar. ¿Qué lo causa?
El servidor renderiza sin conocer el tema del usuario. El cliente aplica después el tema correcto, provocando un flash visible. Solución: usa next-themes o añade un <script> bloqueante en <head> que establezca la clase antes del pintado.
Trampa: Los colores del modo oscuro se ven apagados tras cambiar el tono 500 por el 400. ¿Por qué?
La luminosidad oklch no es simétrica. Cambiar solo el número de tono no garantiza buen contraste en modo oscuro. Prueba los colores visualmente en lugar de invertir números.
¿Cómo tipar la unión Theme en TypeScript?
type Theme = "light" | "dark" | "system";Para next-themes, importa ThemeProviderProps desde next-themes para soporte de tipos completo.
¿Cómo crear un mapa de colores de tema con tipado seguro en TypeScript?
const themeColors = {
light: { bg: "bg-white", text: "text-gray-900" },
dark: { bg: "bg-gray-950", text: "text-gray-100" },
} as const satisfies Record<string, { bg: string; text: string }>;Relacionado
- Setup — Configuración del tema de Tailwind v4
- Custom Utilities — crear utilidades conscientes del tema
- shadcn Setup — tematización de shadcn con modo oscuro
- Utilities — clases de utilidad principales