Módulo 6: Responsive And Dark Mode
Presentación del módulo: responsive y dark mode en el sistema
De "componentes con variantes" a "componentes que además responden a tamaño y tema"
En el módulo 5 cerraste la capa de componentes. El Button de Mercado dejó de ser una cadena de clases suelta y se volvió un componente con variantes (variant: primary/secondary/ghost; size: sm/md/lg) resueltas por props, sobre los tokens del módulo 2, las utilidades de Tailwind del módulo 3 y las escalas del módulo 4. Hoy sabes pedirle a un componente "primary, grande" y recibir la combinación correcta de clases, sin duplicar nada.
Pero fíjate en lo que ese Button —y el ProductCard, y el catálogo entero— todavía no hacen. No se adaptan al tamaño de la pantalla: el catálogo que se ve bien en desktop, con cuatro columnas de tarjetas, en un teléfono se aprieta en cuatro columnas ilegibles. Y no se adaptan al tema: el bg-surface blanco que funciona de día enceguece de noche, y el usuario que puso su sistema en modo oscuro recibe una app que no lo respeta. Las variantes del módulo 5 resolvieron la apariencia por props —el look que tú eliges al montar el componente—. Faltan dos apariencias más que el componente debe manejar por contexto, no por props: la del tamaño y la del tema.
Esa es la capa que este módulo agrega, y la clave está en cómo lo hace: sin duplicar el componente. No vas a construir un ProductCardMobile y un ProductCardDesktop, ni un ProductCard claro y un ProductCardDark. Vas a tener un ProductCard cuya class="" lleva prefijos que Tailwind resuelve según el contexto: prefijos responsive (sm:, md:, lg:) que agregan estilos a partir de cierto ancho, y la variante dark: (o mejor, el theming por tokens del módulo 2) que cambia el color según el tema. Una sola cadena de clases que cubre móvil y desktop, claro y oscuro, todo a la vez.
Conexión con el módulo. Esta es la lección-mapa del módulo 6. No entra a fondo en ninguna pieza: instala la tesis (un componente responde a tamaño y tema por prefijos y tokens, sin duplicarse), da el mapa de las ocho lecciones, y ejecuta un primer teaser del resolveClasses —el modelo pedagógico del cascade de prefijos de Tailwind que usarás todo el módulo—. La lección 2 abre los prefijos responsive y el enfoque mobile-first. La 3 los presenta como una escala de breakpoints compartida por todo el sistema. La 4 introduce la variante dark:. La 5 compara las dos estrategias de dark mode (media vs class). La 6 conecta el dark con el theming por tokens del módulo 2 (el que evita el dark: por elemento). La 7 junta todo: un componente responsive y dark sin duplicación. Y la 8 te pone a hacer el catálogo de Mercado responsive y con dark mode.
Este módulo se apoya en dos cosas que ya tienes. Del ecosistema, en web-fundamentals-html-css (módulo 7, "Responsive design"): ahí aprendiste el CSS responsive a mano —el viewport, las media queries, mobile-first, elegir breakpoints—. Aquí no se re-enseña esa mecánica; se ve cómo Tailwind la expresa con prefijos, y se remite a web-fundamentals para el fondo. Y del módulo 2 de esta guía, el theming por tokens: dark: sobre tokens es el mismo "cambiar el valor del token semántico, no el componente" que ya ejecutaste con resolveToken. Como en todo el módulo, el JSX y el tailwind.config reales se muestran en los bloques (es lo que escribes); la resolución de prefijos se ejecuta en Node, porque el navegador no corre en un agente.
Una analogía: la casa que se amplía y la lámpara que cambia de foco
Piensa en dos decisiones distintas sobre una misma casa.
La primera es el tamaño. Cuando construyes con poco terreno, levantas la casa base: una recámara, una cocina, un baño —lo esencial, lo que cabe en el lote chico—. Si más adelante consigues terreno alrededor, no derrumbas la casa para hacer otra: le agregas cuartos hacia afuera —un estudio cuando hay algo de espacio, una sala grande cuando hay mucho—. La casa base sigue ahí; el terreno extra solo suma. Eso es mobile-first: la base de tus clases es la versión móvil (el lote chico), y los prefijos (md:, lg:) son los cuartos que se agregan cuando hay pantalla para ellos. No diseñas para desktop y recortas hacia el teléfono; diseñas para el teléfono y amplías hacia el desktop.
La segunda decisión es la luz. Cuando quieres que la casa se vea distinta de noche, no la re-decoras entera —no cambias los muebles, ni las paredes, ni los cuadros—. Cambias el foco de las lámparas: un foco cálido y tenue en vez del brillante de día. Los muebles son los mismos; lo que cambia es qué luz los baña. Eso es el dark mode por tokens: no construyes un componente oscuro aparte, cambias el valor de los tokens de color (el "foco" del sistema) y los mismos componentes se ven oscuros. Es exactamente la obra de teatro con dos iluminaciones del módulo 2, ahora expresada en Tailwind.
Y hay una tercera pieza pequeña pero importante: quién decide encender la luz de noche. Puede ser automático —una lámpara con sensor que se prende sola al atardecer— o manual —un interruptor que tú accionas cuando quieres—. En dark mode, "automático" es la estrategia media (sigue el modo oscuro del sistema operativo, vía prefers-color-scheme) y "manual" es la estrategia class (un toggle en la página que agrega o quita .dark). Las dos son válidas; eliges según si quieres que el usuario controle el tema o que solo siga a su sistema.
Guarda las tres imágenes: responsive = agregar cuartos a la casa base cuando hay terreno (los prefijos suman hacia arriba); dark por tokens = cambiar el foco de la lámpara, no re-decorar; estrategia media/class = sensor automático vs interruptor manual. Todo el módulo desarrolla esas tres ideas.
Ejemplo trabajado: una sola cadena, dos contextos
El corazón del módulo es una función: dada una lista de clases con prefijos y un contexto (viewport, theme), decide cuáles quedan activas. Este teaser la muestra con lo mínimo. Definimos la class="" del product-card —una sola cadena— y le preguntamos qué queda activo en dos contextos opuestos: un teléfono en modo claro y un desktop en modo oscuro. Fíjate en que no hay dos componentes: hay una cadena y dos preguntas.
// L1 intro — teaser: una sola cadena de clases responde a tamano Y a tema.
// resolveClasses es un MODELO PEDAGOGICO del cascade de prefijos de Tailwind (no es Tailwind).
const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 };
function propertyOf(base) {
if (['flex', 'inline-flex', 'grid', 'block'].includes(base)) return 'display';
if (base.startsWith('grid-cols-')) return 'grid-cols';
if (base.startsWith('gap-')) return 'gap';
if (base.startsWith('p-')) return 'padding';
if (base.startsWith('bg-')) return 'background';
if (/^text-(xs|sm|base|lg|xl|2xl|3xl)$/.test(base)) return 'font-size';
if (base.startsWith('text-')) return 'text-color';
return base; // por defecto, cada utilidad es su propia "propiedad" (no choca con nadie)
}
function parseClass(raw) {
const parts = raw.split(':');
const base = parts.pop();
let bp = null, dark = false;
for (const p of parts) { if (p in BREAKPOINTS) bp = p; else if (p === 'dark') dark = true; }
return { raw, base, bp, dark };
}
function resolveClasses(classList, { viewport, theme }) {
const parsed = classList.split(/\s+/).filter(Boolean).map(parseClass);
const active = parsed.filter(c =>
(c.bp === null || viewport >= BREAKPOINTS[c.bp]) && (!c.dark || theme === 'dark'));
const winners = new Map();
for (const c of active) {
const prop = propertyOf(c.base);
const score = (c.bp ? BREAKPOINTS[c.bp] : 0) * 2 + (c.dark ? 1 : 0);
const cur = winners.get(prop);
if (!cur || score > cur.score) winners.set(prop, { raw: c.raw, score });
}
const keep = new Set([...winners.values()].map(w => w.raw));
return parsed.filter(c => keep.has(c.raw)).map(c => c.raw);
}
// el product-card de Mercado: UNA sola cadena de clases, sin duplicar el componente.
const card = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 p-4 bg-white dark:bg-gray-900';
console.log('=== la misma class="" en dos contextos distintos ===\n');
console.log('movil + claro (360px, light):');
console.log(' ' + resolveClasses(card, { viewport: 360, theme: 'light' }).join(' '));
console.log('\ndesktop + oscuro (1200px, dark):');
console.log(' ' + resolveClasses(card, { viewport: 1200, theme: 'dark' }).join(' '));
Declaremos qué es y qué no es esto: resolveClasses es un modelo pedagógico del cascade de prefijos de Tailwind. Tailwind y el navegador no corren en un agente, así que el JSX y el tailwind.config reales se muestran en los bloques (es lo que escribes) y aquí ejecutamos la lógica que decide qué clase queda activa para un contexto —lo que el navegador hace por debajo con media queries y la clase .dark—.
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== la misma class="" en dos contextos distintos ===
movil + claro (360px, light):
grid grid-cols-1 p-4 bg-white
desktop + oscuro (1200px, dark):
grid lg:grid-cols-4 p-4 dark:bg-gray-900
Lee las dos salidas como el resumen del módulo. La cadena de entrada fue una sola —grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 p-4 bg-white dark:bg-gray-900— y produjo dos resultados distintos según el contexto. En el teléfono en modo claro (360px, light), quedaron activas grid-cols-1 (una columna, porque los prefijos md: y lg: piden más ancho del que hay) y bg-white (fondo blanco, porque dark: no aplica en tema claro). En el desktop en modo oscuro (1200px, dark), quedaron lg:grid-cols-4 (cuatro columnas, porque a 1200px el ancho supera el breakpoint lg) y dark:bg-gray-900 (fondo oscuro, porque ahora el tema es dark). Las clases que no dependen del contexto —grid, p-4— aparecen en las dos. Una cadena, dos contextos, dos apariencias, y cero duplicación: no escribiste una tarjeta para móvil y otra para desktop, ni una clara y otra oscura.
Fíjate en el mecanismo que esto revela y que el módulo entero desarrolla. Los prefijos no son clases distintas que "compiten": son la misma propiedad (el número de columnas) resuelta según cuánto ancho hay. La versión sin prefijo (grid-cols-1) es la base —la que aplica siempre, empezando por el móvil—; cada prefijo la reemplaza hacia arriba cuando su breakpoint se cumple. Eso es mobile-first, y es lo que hace que agregar md:grid-cols-2 sea "agregar un cuarto cuando hay terreno", no "rehacer la casa".
Una pregunta para cargar por el resto del módulo: en la segunda salida, ¿por qué dark:bg-gray-900 en vez de que bg-white cambiara de valor solo? (Aquí usamos la variante dark: explícita —una clase distinta para el tema oscuro—. Pero hay otra forma, más de sistema: que bg-surface apunte a un token que cambia de valor bajo .dark, sin escribir un dark: por elemento. Adelanto: es el theming por tokens del módulo 2, y lo construyes en la lección 6.)
El mapa del módulo
Guarda esta ruta; es cómo cada lección construye una parte de la capa responsive + dark:
Idea Leccion Concepto clave
────────────────────────────────────────── ──────── ─────────────────────────────────────
Prefijos responsive, mobile-first L2 base = movil; sm:/md:/lg: agregan
estilos hacia arriba (min-width)
Los breakpoints como escala del sistema L3 sm 640 / md 768 / lg 1024 / xl 1280;
un set compartido, no valores sueltos
La variante dark: L4 dark:bg-gray-900: otra utilidad cuando
el tema es oscuro
media vs class (dos estrategias) L5 prefers-color-scheme (auto) vs .dark en
<html> (toggle manual)
Theming por tokens en dark L6 bg-surface cambia de VALOR bajo .dark;
sin dark: por elemento (modulo 2)
Responsive + dark sin duplicar L7 una class="" cubre N anchos x 2 temas;
un componente, no cuatro
────────────────────────────────────────── ──────── ─────────────────────────────────────
Proyecto: el catalogo de Mercado L8 product-card responsive (1->N columnas)
+ dark por toggle .dark + tokens
La frontera: qué NO entra en este módulo
Saber la frontera te evita mezclar lo que otras lecciones y guías cubren:
- El CSS responsive a mano —el viewport y su
<meta>, las media queries@media (min-width: ...), las unidades fluidas (rem,vw,clamp), las imágenes responsive— esweb-fundamentals-html-css, módulo 7. Aquí asumimos que sabes qué es una media query y qué es mobile-first; lo que hacemos es ver cómo Tailwind lo expresa con prefijos. Si "min-width" o "mobile-first" te suenan nuevos, ese es el módulo que falta, no este. - La mecánica de los tokens y el theming —primitivos vs semánticos vs de componente,
resolveToken, el override por tema— es el módulo 2 de esta guía. Aquí lo reusamos para el dark por tokens (lección 6); no lo re-enseñamos. - Las variantes de componente por props (
variant,size, el patrón cva) son el módulo 5. Las variantes de breakpoint (md:) y de tema (dark:) de este módulo son otra cosa: no las eliges al montar el componente, las resuelve el contexto. Se usan juntas, no se pisan. - Las primitivas accesibles (shadcn/Radix) son el módulo 7. Aquí no.
- El bundle de CSS y el purgado —cómo Tailwind elimina las clases no usadas para que la hoja final sea pequeña— es
fullstack-performance-and-deployment. Aquí lo mencionamos y remitimos.
Errores comunes
Creer que responsive y dark se hacen duplicando el componente. Qué pasa: para el móvil se crea un ProductCardMobile, para el desktop un ProductCardDesktop; para el tema oscuro, un ProductCardDark. Por qué pasa: "necesito que se vea distinto → hago otro componente" es lo más literal. Cómo detectarlo: tienes dos o cuatro componentes que solo difieren en unas clases de layout o color, y cada cambio de forma lo tienes que hacer en todos. Cómo corregirlo: un componente con una class="" que lleva prefijos (md:, dark:) y tokens; el contexto decide cuál aplica. Duplicar el componente por tamaño o por tema multiplica el mantenimiento —es el error que este módulo entero existe para eliminar—.
Diseñar desktop-first y pelear la cascada con max-*. Qué pasa: se escribe el layout de desktop como base y luego se "arregla" para móvil con prefijos de máximo ancho (max-md:). Por qué pasa: uno diseña mirando su monitor grande. Cómo detectarlo: tu class="" está llena de overrides max-* que deshacen la base, y el móvil se siente como una excepción remendada. Cómo corregirlo: mobile-first —la base es el móvil, los prefijos sm:/md:/lg: agregan hacia arriba—. La lección 2 mide por qué la cascada de min-width se lee mejor que la de max-width; el fondo está en web-fundamentals-html-css módulo 7.
Esperar re-aprender el CSS responsive aquí. Qué pasa: alguien abre el módulo esperando entender el viewport, las media queries o clamp. Por qué pasa: "responsive" suena a que aquí se enseña desde cero. Cómo detectarlo: te trabas en qué es una media query, no en cómo el prefijo md: la genera. Cómo corregirlo: eso es web-fundamentals-html-css módulo 7, el prerequisito. Aquí lo damos por sabido y nos concentramos en cómo el sistema lo expresa.
Ejercicios
Ejercicio 1 — ¿Prefijo o componente nuevo? Para cada situación, di si la resuelve un prefijo/token en la misma class="" (el enfoque del módulo) o si describe una duplicación de componente (lo que el módulo evita):
- (a) El catálogo muestra 1 columna en móvil y 4 en desktop.
- (b) Existe un
ProductCardDark.jsxcon los colores oscuros escritos, aparte delProductCard.jsxclaro. - (c) El fondo de la tarjeta es blanco de día y casi negro de noche, con
bg-surfaceapuntando a un token que cambia de valor. - (d) Hay un
Navbarpara móvil y unNavbarDesktopcompletamente separados.
Ver solución
- (a) Prefijo. Una sola clase de grid con prefijos:
grid-cols-1 md:grid-cols-4. El número de columnas es la misma propiedad resuelta por ancho; no hay dos catálogos. - (b) Duplicación. Dos componentes que solo difieren en color, y cada cambio de forma hay que hacerlo en los dos. Es justo lo que el theming por tokens (lección 6) reemplaza: un
ProductCardconbg-surface, y el token cambia bajo.dark. - (c) Token. El enfoque de sistema: la utilidad no cambia (
bg-surface), cambia el valor del token por tema. Ni siquiera necesita undark:por elemento. - (d) Duplicación. Dos navbars separados. A veces el layout móvil y desktop difieren tanto que se justifica más lógica, pero "completamente separados" suele ser el olor de que faltó resolver con prefijos y composición.
La pregunta guía: ¿la variación entra por un prefijo/token en la misma cadena, o por duplicar la pieza? Lo primero es sistema; lo segundo es la estantería que crece.
Ejercicio 2 — Predice el teaser. Con el resolveClasses del ejemplo trabajado y la cadena card, sin correr nada, di qué clases quedan activas para un tablet en modo claro: { viewport: 768, theme: 'light' }.
Ver solución
Quedan activas: grid grid-cols-... p-4 bg-white, con el grid resuelto a md:grid-cols-2. A 768px, el breakpoint md (768) se cumple (768 >= 768) pero lg (1024) no, así que la propiedad grid-cols la gana md:grid-cols-2, reemplazando a la base grid-cols-1. El tema es claro, así que bg-white gana y dark:bg-gray-900 queda fuera. grid y p-4 no dependen del contexto y siempre están. Resultado: grid md:grid-cols-2 p-4 bg-white. La lección: cada propiedad se resuelve por separado —el ancho decide el grid, el tema decide el fondo— y una sola cadena cubre las dos dimensiones a la vez.
Ejercicio 3 — Ubica la afirmación. Para cada afirmación sobre este módulo 6, di si es verdadera o falsa y por qué:
- (a) "Los prefijos responsive de Tailwind son azúcar sobre las media queries que aprendiste en web-fundamentals."
- (b) "Para tener dark mode hay que escribir cada componente dos veces."
- (c) "Las variantes
variant/sizedel módulo 5 se reemplazan por los prefijosmd:/dark:de este módulo."
Ver solución
- (a) Verdadera.
md:p-4genera, por debajo, una regla dentro de@media (min-width: 768px). El prefijo es la forma de Tailwind de escribir la media query que en web-fundamentals módulo 7 escribías a mano; el mecanismo del navegador es el mismo. - (b) Falsa. Todo el módulo existe para no duplicar: la variante
dark:(o mejor, el token que cambia de valor) hace que un componente cubra los dos temas. Escribir cada componente dos veces es exactamente el error que se evita. - (c) Falsa. No se reemplazan: se complementan.
variant/sizeson variantes de apariencia que tú eliges por props al montar el componente;md:/dark:son variantes de contexto que resuelve el ancho y el tema. UnButtonpuede tener las dos a la vez —variant="primary"y ademásclass="text-base md:text-lg"—.
Resumen y siguiente paso
En esta lección instalaste la tesis del módulo 6: un componente responde al tamaño de la pantalla y al tema por prefijos y tokens en la misma class="", sin duplicarse. Con la casa que se amplía y la lámpara que cambia de foco viste las dos ideas que estructuran el módulo: mobile-first es agregar cuartos a la casa base cuando hay terreno (los prefijos suman hacia arriba), y el dark por tokens es cambiar el foco de la lámpara, no re-decorar la casa. Y lo comprobaste ejecutando: una sola cadena de clases del product-card resolvió dos apariencias opuestas —una columna y fondo blanco en el móvil claro, cuatro columnas y fondo oscuro en el desktop dark— sin un solo componente duplicado.
Antes de avanzar deberías poder: explicar por qué responsive y dark se hacen con prefijos/tokens y no duplicando el componente; nombrar las dos dimensiones que este módulo agrega (tamaño y tema) y cómo se distinguen de las variantes por props del módulo 5; y ubicar el CSS responsive a mano como el prerequisito de web-fundamentals módulo 7.
La lección 2 baja al primer mecanismo concreto: los prefijos responsive y el enfoque mobile-first. Antes de mezclar tamaño y tema hay que ver bien cómo p-2 md:p-4 lg:p-6 construye una cascada de min-width —la base móvil y los prefijos que agregan hacia arriba— y por qué eso es mobile-first y no al revés. Es el cimiento sobre el que el resto del módulo se apoya.
Recursos
- Tailwind CSS, "Responsive Design" — tailwindcss.com/docs/responsive-design. El documento base del módulo: los prefijos
sm:/md:/lg:, el enfoque mobile-first y los breakpoints por defecto. En inglés. - Tailwind CSS, "Dark Mode" — tailwindcss.com/docs/dark-mode. La variante
dark:y las dos estrategias (mediayclass) que verás en las lecciones 4 y 5. En inglés. - web.dev, "prefers-color-scheme: Hello darkness, my old friend" — web.dev/articles/prefers-color-scheme. La guía de referencia sobre cómo diseñar para modo oscuro en la web; el fondo de la estrategia
media. En inglés. - MDN, "Responsive design" — developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design. El repaso del CSS responsive a mano (viewport, media queries, mobile-first) que este módulo asume; el prerequisito de web-fundamentals. En inglés.