En esta página
Dark mode y variantes avanzadas
Dark mode y variantes avanzadas en Tailwind
Una de las características más potentes de Tailwind es su sistema de variantes: prefijos que aplican estilos condicionalmente según el estado del elemento, el estado del DOM, o las preferencias del usuario. El dark mode es solo una de las muchas variantes disponibles.
Dark mode con la variante dark:
Estrategia media (prefers-color-scheme)
Por defecto, Tailwind usa la preferencia de color del sistema operativo:
<!-- Se oscurece automáticamente cuando el OS está en modo oscuro -->
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<h1 class="text-3xl font-bold">Título</h1>
<p class="text-gray-600 dark:text-gray-400">Descripción</p>
</body>No necesitas ninguna configuración extra. Tailwind escucha @media (prefers-color-scheme: dark) automáticamente.
Estrategia class (toggle manual)
Para implementar un toggle de tema que el usuario pueda controlar:
/* styles.css */
@import "tailwindcss";
@theme {
/* Configurar la estrategia 'class' para dark mode */
}
/* Tailwind v4: configura dark mode con selector */
@custom-variant dark (&:is(.dark *));Con esta configuración, añades/quitas la clase dark al elemento <html>:
<!-- Modo claro -->
<html class="">...</html>
<!-- Modo oscuro -->
<html class="dark">...</html>// Toggle con JavaScript
const toggle = document.querySelector('#theme-toggle');
toggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
// Persiste en localStorage
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
// Restaurar al cargar
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark');
}Paleta de colores para dark mode
El truco es definir una paleta de superficies semántica:
<!-- Patrones de color para dark mode -->
<!-- Fondo principal -->
<div class="bg-white dark:bg-gray-950">...</div>
<!-- Fondo de tarjetas/componentes -->
<div class="bg-gray-50 dark:bg-gray-900">...</div>
<!-- Fondo elevado (sobre tarjetas) -->
<div class="bg-white dark:bg-gray-800">...</div>
<!-- Bordes -->
<div class="border border-gray-200 dark:border-gray-700">...</div>
<!-- Texto principal -->
<p class="text-gray-900 dark:text-white">...</p>
<!-- Texto secundario -->
<p class="text-gray-600 dark:text-gray-400">...</p>
<!-- Texto de placeholder/deshabilitado -->
<p class="text-gray-400 dark:text-gray-600">...</p>Botón de toggle de tema completo
<!-- Botón de toggle de tema -->
<button
id="theme-toggle"
type="button"
class="relative w-12 h-6 rounded-full transition-colors duration-300
bg-gray-200 dark:bg-blue-600
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
aria-label="Cambiar tema"
>
<!-- Círculo que se desliza -->
<span
class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-sm
transition-transform duration-300
translate-x-0 dark:translate-x-6"
></span>
<!-- Iconos de sol/luna -->
<span class="absolute inset-0 flex items-center justify-start pl-1.5 text-xs dark:hidden">
☀️
</span>
<span class="absolute inset-0 flex items-center justify-end pr-1.5 text-xs hidden dark:flex">
🌙
</span>
</button>Variante group:
Ya vista brevemente antes, group es esencial para efectos hover complejos:
<!-- Grupo con múltiples hijos que reaccionan -->
<div class="group rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-shadow">
<!-- La imagen escala en hover del padre -->
<div class="overflow-hidden h-48">
<img
src="course.jpg"
alt="Portada"
class="w-full h-full object-cover transition-transform duration-500
group-hover:scale-110"
/>
</div>
<!-- El badge cambia de color -->
<div class="p-5">
<span
class="text-xs font-semibold px-2 py-1 rounded-full
bg-blue-100 text-blue-700
group-hover:bg-blue-600 group-hover:text-white
transition-colors"
>
Nuevo
</span>
<h3 class="font-bold text-gray-900 mt-2 group-hover:text-blue-600 transition-colors">
Título del curso
</h3>
</div>
</div>Grupos anidados con group/{name}
<!-- Múltiples grupos en el mismo DOM -->
<div class="group/card rounded-xl p-6 hover:bg-blue-50">
<div class="group/header flex items-center justify-between mb-4">
<h3 class="font-bold group-hover/card:text-blue-600">Título</h3>
<button
type="button"
class="opacity-0 group-hover/header:opacity-100
group-hover/card:opacity-50 hover:!opacity-100"
>
✏️
</button>
</div>
<p class="text-gray-500">Contenido de la tarjeta</p>
</div>Variante peer:
peer permite que un elemento aplique estilos a su hermano siguiente según su estado:
<!-- Validación de formulario con peer -->
<div>
<input
type="email"
class="peer w-full border rounded-lg px-4 py-2 outline-none
border-gray-300 focus:border-blue-500
invalid:border-red-400 invalid:bg-red-50"
placeholder="[email protected]"
required
/>
<!-- Este párrafo es hermano del input — usa peer-invalid: -->
<p
class="mt-1 text-sm text-red-500
invisible peer-invalid:visible"
>
Por favor ingresa un email válido
</p>
</div>
<!-- Checkbox personalizado con peer -->
<label class="flex items-center gap-3 cursor-pointer select-none">
<input type="checkbox" class="peer sr-only" />
<div
class="w-5 h-5 rounded border-2 border-gray-300
peer-checked:bg-blue-600 peer-checked:border-blue-600
peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500
transition-colors flex items-center justify-center"
>
<svg
class="w-3 h-3 text-white opacity-0 peer-checked:opacity-100 transition-opacity"
viewBox="0 0 12 12" fill="currentColor"
>
<path d="M10 3L5 8.5 2 5.5l-1 1 4 4 6-7-1-1z"/>
</svg>
</div>
<span class="text-gray-700 peer-checked:text-blue-700 transition-colors">
Acepto los términos y condiciones
</span>
</label>Variante has:
has: aplica estilos a un padre basándose en sus descendientes:
<!-- Formulario que cambia cuando tiene un input con focus -->
<div
class="border rounded-xl p-4 transition-colors
border-gray-200
has-[input:focus]:border-blue-400
has-[input:focus]:bg-blue-50/30"
>
<label class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input
type="email"
class="w-full outline-none bg-transparent text-gray-900 placeholder:text-gray-400"
placeholder="[email protected]"
/>
</div>
<!-- Tarjeta seleccionable -->
<label
class="block border-2 rounded-xl p-6 cursor-pointer
border-gray-200 hover:border-gray-300
has-[input:checked]:border-blue-500 has-[input:checked]:bg-blue-50
transition-colors"
>
<input type="radio" name="plan" value="pro" class="sr-only" />
<div class="flex items-center justify-between">
<div>
<p class="font-semibold text-gray-900">Plan Pro</p>
<p class="text-sm text-gray-500">$19/mes</p>
</div>
<div
class="w-5 h-5 rounded-full border-2 border-gray-300
has-[input:checked]/label:border-blue-500"
></div>
</div>
</label>Variantes de datos: data-*
<!-- Activar variantes con atributos data-* -->
<div data-state="open" class="data-[state=open]:block hidden">
Panel visible cuando data-state="open"
</div>
<button
data-loading="true"
class="bg-blue-600 text-white px-4 py-2 rounded-lg
data-[loading=true]:opacity-50 data-[loading=true]:cursor-not-allowed"
>
Enviar
</button>
<!-- Variantes ARIA -->
<div
role="button"
aria-expanded="false"
class="aria-expanded:rotate-180 transition-transform"
>
▼
</div>Variantes de medios: motion-safe y motion-reduce
<!-- Respetar las preferencias de movimiento del usuario -->
<div
class="motion-safe:transition-transform motion-safe:hover:scale-105
motion-reduce:transition-none"
>
<!-- Se anima solo si el usuario no tiene reducción de movimiento activa -->
</div>
<div
class="motion-safe:animate-bounce motion-reduce:opacity-75"
>
Indicador de carga
</div>Variantes print
<!-- Estilos solo para impresión -->
<nav class="print:hidden">
<!-- La navegación no se imprime -->
</nav>
<article class="print:text-black print:bg-white">
<!-- El artículo se imprime limpio -->
</article>Variante forced-colors
<!-- Accesibilidad en modo de alto contraste de Windows -->
<button
class="bg-blue-600 text-white border border-transparent
forced-colors:border-current"
>
Botón accesible en alto contraste
</button>Resumen
Las variantes de Tailwind transforman HTML semántico en interfaces reactivas sin JavaScript para los estados CSS. Las más importantes son: dark: para modo oscuro, group-hover: para efectos padre-hijo, peer-checked: para formularios personalizados, has: para estilos basados en descendientes, y data-[attr=value]: para integración con JavaScript. La variante motion-reduce: es crucial para accesibilidad.
<!-- dark: aplica cuando el elemento ancestro tiene class="dark" -->
<!-- O cuando el sistema operativo tiene modo oscuro (media strategy) -->
<div class="bg-white dark:bg-gray-900 min-h-screen">
<!-- Tarjeta con dark mode -->
<div
class="bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
rounded-xl p-6 shadow-sm"
>
<h2 class="text-gray-900 dark:text-white font-bold text-xl mb-3">
Título de la tarjeta
</h2>
<p class="text-gray-600 dark:text-gray-400 leading-relaxed">
Contenido que se adapta automáticamente al modo oscuro.
</p>
<button
class="mt-4 bg-blue-600 hover:bg-blue-700
dark:bg-blue-500 dark:hover:bg-blue-400
text-white font-medium px-4 py-2 rounded-lg
transition-colors"
>
Acción principal
</button>
</div>
</div>
Inicia sesión para guardar tu progreso