Módulo 6: Responsive And Dark Mode

Los breakpoints como una escala del sistema

Descripción

En la lección anterior usaste md: y lg: sin preguntarte de dónde salen esos anchos. Aquí los miras de frente: sm, md, lg, xl son un conjunto de breakpoints —anchos concretos (640, 768, 1024, 1280 px) con nombres— que Tailwind trae por defecto y que todo tu proyecto comparte. Y ese "todo comparte" es la idea central de la lección: los breakpoints no son valores sueltos que eliges de nuevo en cada componente; son una escala, igual que la escala de espaciado y la tipográfica del módulo 4. Cuando el catálogo quiebra a 4 columnas en lg y el navbar cambia de layout en lg, los dos usan el mismo 1024px —no uno 1000 y otro 1050—, y por eso el storefront entero se reordena de forma coherente al cambiar de tamaño.

Una escala de breakpoints hace por el layout lo que la escala de espaciado hace por el aire: reemplaza mil decisiones ad-hoc por un juego pequeño de valores con nombre. En vez de que cada quien invente el ancho donde su componente cambia —"este a 700, aquel a 900, el otro a 1100"—, todos toman del mismo set, y la UI quiebra en los mismos puntos. Menos valores, más coherencia: es exactamente el argumento del módulo 4, aplicado al eje del tamaño de pantalla.

Conexión con el módulo. Esta lección organiza los prefijos de la lección 2 como una escala. Se apoya en el módulo 4 de esta guía (las escalas dan coherencia: pocos valores con nombre en vez de decisiones sueltas) y en web-fundamentals-html-css módulo 7 ("Choosing breakpoints"), donde viste que los breakpoints deben salir del contenido, no de los tamaños de dispositivos concretos: aquí no re-enseñamos eso; vemos el set por defecto de Tailwind, cómo se personaliza en el tailwind.config, y cómo el catálogo de Mercado usa el mismo set en todos lados. La resolución de qué columna aplica por ancho se ejecuta en Node.

Una analogía: las tallas de ropa estandarizadas

Piensa en cómo una marca de ropa maneja las tallas. Podría, en teoría, coser cada prenda a una medida distinta —esta a 96cm de pecho, aquella a 98, la otra a 101—: un caos de medidas irrepetibles donde nada combina y el inventario es imposible. Lo que hace en cambio es adoptar una escala de tallas estandarizada: S, M, L, XL, con medidas fijas y conocidas. Toda la ropa de la marca usa esa escala. Una playera M y un pantalón M están pensados para el mismo cuerpo; un abrigo L combina con una camisa L porque los dos hablan la misma escala.

Los breakpoints de Tailwind son esa escala de tallas, pero para el ancho de pantalla. sm, md, lg, xl son las "tallas" del sistema, con medidas fijas (640, 768, 1024, 1280). Todo componente del storefront usa esa escala: cuando el catálogo cambia a md y el navbar cambia a md, los dos hablan del mismo ancho —768px—, como una playera M y un pantalón M hablan del mismo cuerpo. No hay una prenda cosida a una medida rara que no combine con nada.

La moraleja es la misma que la de toda escala: pocos valores compartidos ganan a muchos valores sueltos. Si cada componente inventara su propio ancho de quiebre, tu UI se reordenaría a los tirones —uno cambia a 700, otro a 750, otro a 820— y nunca se sentiría coherente. Con una escala de breakpoints, todo quiebra en los mismos cuatro o cinco puntos, y la página se reacomoda como una sola pieza. La coherencia del layout responsive sale de compartir la escala, igual que la coherencia del espaciado sale de compartir la escala de espaciado.

El mecanismo: el set por defecto y cómo se personaliza

Tailwind trae este set de breakpoints, y son min-widths (a partir de, como viste en la lección 2):

Nombre   min-width   Pensado para (aproximado)
──────   ─────────   ─────────────────────────
sm       640px       tablet chico / telefono apaisado
md       768px       tablet
lg       1024px      laptop / desktop chico
xl       1280px      desktop
2xl      1536px      desktop grande

Estos valores viven en el tailwind.config y puedes cambiarlos o ampliarlos. Si tu diseño necesita otro punto de quiebre, lo defines una vez, ahí, y queda disponible como prefijo en todo el proyecto:

// tailwind.config.js — la escala de breakpoints del proyecto (esto se MUESTRA)
export default {
  theme: {
    screens: {
      sm: '640px',
      md: '768px',
      lg: '1024px',
      xl: '1280px',
      // podrias agregar uno propio si el contenido lo pide:
      // '3xl': '1920px',
    },
  },
};

Fíjate en el paralelo con el módulo 4. Igual que la escala de espaciado vivía en el theme y se consumía con p-4, gap-2, la escala de breakpoints vive en theme.screens y se consume con los prefijos md:, lg:. Cambiar un breakpoint en el config lo cambia en todo el proyecto de golpe —el mismo beneficio de fuente única que tienen los tokens—. Nadie escribe @media (min-width: 768px) a mano en un componente; escribe md:, y el número vive en un solo lugar.

Y una advertencia que viene de web-fundamentals módulo 7: los nombres sm/md/lg sugieren dispositivos, pero los breakpoints no deben elegirse "por dispositivo". No pongas un quiebre "para iPhone" y otro "para iPad" —hay demasiados tamaños y cambian cada año—. El quiebre va donde tu contenido se rompe: donde las tarjetas quedan demasiado angostas, donde una línea de texto se vuelve incómodamente larga. La escala por defecto de Tailwind es un buen punto de partida porque cubre los saltos típicos, pero el criterio de dónde quebrar es el contenido, no el catálogo de dispositivos. Ese criterio lo desarrollaste en web-fundamentals; aquí lo aplicas con la escala de Tailwind.

Ejemplo trabajado: el catálogo de Mercado, de 1 a 2 a 4 columnas

El catálogo de Mercado es una rejilla de product-cards. En un teléfono, una tarjeta por fila (una columna); en un tablet, dos; en un desktop, cuatro. Una sola cadena de grid, con prefijos de la escala, lo resuelve. Ejecutamos resolveClasses sobre grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 a tres anchos para ver qué número de columnas queda activo:

// L3 — los breakpoints como una ESCALA compartida del sistema (modelo pedagogico).
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;
}
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 grid del catalogo de Mercado: 1 columna en movil, 2 en tablet, 4 en desktop.
const grid = 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4';
console.log('=== la misma escala de breakpoints gobierna todo el sistema ===\n');
console.log('breakpoints (min-width, px):', JSON.stringify(BREAKPOINTS));
console.log('cadena: "' + grid + '"\n');
for (const viewport of [360, 700, 1200]) {
  const active = resolveClasses(grid, { viewport, theme: 'light' });
  console.log(`viewport ${String(viewport).padStart(4)}px -> ${active.join(' ')}`);
}

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== la misma escala de breakpoints gobierna todo el sistema ===

breakpoints (min-width, px): {"sm":640,"md":768,"lg":1024,"xl":1280}
cadena: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"

viewport  360px -> grid-cols-1
viewport  700px -> sm:grid-cols-2
viewport 1200px -> lg:grid-cols-4

Lee las tres líneas como el catálogo reacomodándose. A 360px (teléfono), grid-cols-1: una tarjeta por fila, porque ni sm (640) ni lg (1024) se cumplen. A 700px (tablet chico), sm:grid-cols-2: dos columnas, porque 700 cumple sm (640) pero no lg (1024). A 1200px (desktop), lg:grid-cols-4: cuatro columnas, porque 1200 cumple lg. El catálogo pasó de 1 a 2 a 4 columnas usando tres puntos de la misma escala, con una sola cadena. Fíjate en que saltamos md en el diseño (fuimos de sm a lg directo): no estás obligado a usar todos los breakpoints, solo los puntos donde el contenido pide un cambio —el criterio del contenido, de web-fundamentals—.

Ahora ve el punto de la escala compartida. Los anchos que gobiernan este grid —640, 1024— salen del mismo set BREAKPOINTS que gobernaría el navbar, el footer, el botón de "filtrar". Si mañana el navbar quiebra a sm y el catálogo a sm, los dos usan 640px: cuando el usuario arrastra la ventana y cruza los 640px, ambos reaccionan al mismo tiempo, y la página se reordena como una pieza. Si cada uno hubiera inventado su ancho —el navbar a 600, el catálogo a 680— cruzarías una zona donde uno ya cambió y el otro no: el reacomodo se siente a los tirones. La coherencia del layout responsive es, literalmente, compartir la escala.

Una pregunta para razonar el diseño: en la salida, a 700px hay dos columnas; ¿por qué no elegimos que hubiera dos columnas desde, digamos, 500px? (Porque el breakpoint va donde el contenido lo pide, no en un ancho arbitrario. A 500px, dos product-cards lado a lado quedarían demasiado angostas para mostrar imagen, título y precio con claridad; a 640px ya caben cómodas. El valor sm: 640 de la escala es un buen punto para ese salto. Elegir 500 "porque sí" es volver a la decisión ad-hoc que la escala existe para reemplazar.)

Profundización: cuándo tocar la escala (y cuándo no)

La tentación, al empezar, es agregar breakpoints. "Mi diseño necesita un cambio a 900px" → se agrega un breakpoint a 900. A veces está bien; muchas veces es señal de que estás resolviendo con un quiebre lo que se resolvería con una unidad fluida. Recuerda de web-fundamentals módulo 7 que no todo cambio de tamaño necesita un breakpoint: el ancho de un contenedor puede crecer suave con max-w-* y w-full, el texto puede escalar con clamp, y muchas cosas simplemente fluyen sin ningún quiebre. Los breakpoints son para cambios discretos de layout —de 1 columna a 2, de vertical a horizontal—, no para lo que puede ser continuo.

Cuando sí necesitas tocar la escala, la regla es hacerlo en el tailwind.config, no inventar media queries a mano en un componente. Si un proyecto de verdad necesita un quiebre a 900px de forma recurrente, se agrega screens en el config con un nombre —y desde entonces existe como prefijo en todo el proyecto—. Lo que rompe la coherencia no es tener un breakpoint extra: es tener uno que vive suelto en un componente, invisible para el resto del sistema. La escala compartida sigue siendo compartida solo si los cambios pasan por el config.

Y ten presente el paralelo completo con el módulo 4. Allá viste que una escala tiene un rango (de space-1 a space-16) del que eliges pocos valores; aquí la escala de breakpoints tiene su rango (de sm a 2xl) del que un componente típico usa dos o tres. No usar todos no es "desperdiciar" la escala —igual que no usar todos los pasos de espaciado no la desperdicia—; la escala está para que cuando necesites un quiebre, tomes uno del set compartido en vez de inventarlo.

Errores comunes

Inventar un ancho de quiebre por componente en vez de usar la escala. Qué pasa: un componente escribe min-[680px]:grid-cols-2 con un ancho arbitrario; otro usa min-[720px]:...; un tercero min-[750px]:.... Por qué pasa: cada quien mira su componente aislado y elige "el ancho donde se ve bien" sin coordinar. Cómo detectarlo: tu proyecto tiene media queries con números crudos y distintos por todos lados, y la página se reordena a los tirones al cambiar de tamaño. Cómo corregirlo: usa la escala (sm:, md:, lg:); si de verdad falta un punto, agrégalo al tailwind.config con nombre para que sea compartido. Un breakpoint suelto en un componente es como una prenda cosida a una medida rara: no combina con el resto del guardarropa.

Elegir breakpoints por dispositivo en vez de por contenido. Qué pasa: se ponen quiebres "para iPhone" (375), "para iPad" (768), "para MacBook" (1440), copiando los tamaños de dispositivos de moda. Por qué pasa: es tangible pensar en aparatos concretos. Cómo detectarlo: tus breakpoints persiguen modelos de dispositivos que cambian cada año, y en un tamaño intermedio (una ventana de laptop a media pantalla) el diseño se rompe porque "no es ningún dispositivo". Cómo corregirlo: el quiebre va donde el contenido deja de verse bien —tarjetas demasiado angostas, líneas demasiado largas—, no donde está un aparato. Es el criterio de web-fundamentals módulo 7: content-first, no device-first. La escala por defecto de Tailwind ya está pensada así.

Meter un breakpoint donde bastaba una unidad fluida. Qué pasa: se agregan tres o cuatro breakpoints para ir ajustando el ancho de un contenedor paso a paso. Por qué pasa: el breakpoint es la herramienta que uno ya conoce, así que se usa para todo. Cómo detectarlo: tienes una escalera de breakpoints que solo cambian un max-w-* de a poquito, cuando el contenedor podría crecer suave. Cómo corregirlo: para lo continuo usa unidades fluidas (w-full max-w-4xl, clamp para el texto —web-fundamentals módulo 7—); reserva los breakpoints para los cambios discretos de layout (número de columnas, dirección del flex). No todo necesita un quiebre.

Ejercicios

Ejercicio 1 — Resuelve el grid. Con el resolveClasses del ejemplo y la cadena grid-cols-2 md:grid-cols-3 xl:grid-cols-6, sin correr nada, di cuántas columnas quedan activas en:

  • (a) 500px
  • (b) 900px
  • (c) 1400px
Ver solución

Con la escala (md 768, xl 1280):

viewportactivacolumnaspor qué
500pxgrid-cols-22no cumple md (768); base
900pxmd:grid-cols-33cumple md (768) pero no xl (1280)
1400pxxl:grid-cols-66cumple md y xl; gana xl, el mayor

Fíjate en que la base aquí no es una columna: es grid-cols-2. Mobile-first no obliga a empezar en 1 columna —empieza en lo que el contenido pida para el móvil—; en este diseño, dos columnas angostas caben en el teléfono. La escala solo dice dónde quiebra; cuántas columnas en cada tramo lo decides tú.

Ejercicio 2 — De media queries a mano a la escala. Alguien trae este CSS de un proyecto viejo, con anchos crudos, y quiere pasarlo a prefijos de Tailwind usando la escala por defecto:

.catalog { grid-template-columns: 1fr; }
@media (min-width: 768px) { .catalog { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px){ .catalog { grid-template-columns: repeat(4, 1fr); } }

Escribe la class="" equivalente.

Ver solución
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">...</div>

Cada media query mapea a un prefijo de la escala: min-width: 768pxmd:, min-width: 1024pxlg:. La base sin @media (1fr, una columna) es la base sin prefijo (grid-cols-1). Los anchos crudos del CSS viejo (768, 1024) coinciden con la escala de Tailwind —por eso el mapeo es directo—; ese es el punto de adoptar una escala estándar: los quiebres típicos ya tienen nombre. Si el CSS viejo hubiera usado 750 y 1050, tocaría decidir si redondear a la escala (recomendado) o agregar breakpoints propios al config.

Ejercicio 3 — ¿Breakpoint o unidad fluida? Para cada necesidad, di si se resuelve mejor con un breakpoint (cambio discreto) o con una unidad fluida (cambio continuo):

  • (a) El catálogo pasa de 1 a 3 columnas.
  • (b) El contenedor principal nunca supera 1200px de ancho pero se encoge suave en pantallas menores.
  • (c) La barra lateral desaparece en móvil y aparece en desktop.
  • (d) El tamaño del título crece gradualmente con el ancho de la ventana.
Ver solución
  • (a) Breakpoint. El número de columnas es discreto —se salta de 1 a 3, no hay "2.4 columnas"—. Va con grid-cols-1 lg:grid-cols-3.
  • (b) Unidad fluida. Un ancho que se encoge suave es continuo: w-full max-w-6xl (o max-w-[1200px]). No necesita ningún quiebre; el contenedor fluye hasta su tope.
  • (c) Breakpoint. Aparecer/desaparecer es discreto: hidden lg:block. Un elemento está o no está; no hay estado intermedio.
  • (d) Unidad fluida. Un tamaño que crece gradualmente es el caso de clamp (web-fundamentals módulo 7): text-[clamp(...)]. Con breakpoints darías saltos; con clamp, una curva suave.

La regla: breakpoint para lo discreto (columnas, aparecer/ocultar, dirección del flex), unidad fluida para lo continuo (anchos, tamaños graduales). Mezclar los dos criterios llena el proyecto de breakpoints que sobran.

Resumen y siguiente paso

En esta lección viste que los breakpoints son una escala compartida —sm 640, md 768, lg 1024, xl 1280— y no valores sueltos por componente. Con las tallas de ropa estandarizadas entendiste por qué: cuando todo el storefront quiebra en los mismos puntos, la página se reordena como una pieza; cuando cada componente inventa su ancho, se reordena a los tirones. Viste que la escala vive en tailwind.config (como los tokens del módulo 4), que se personaliza ahí una sola vez para todo el proyecto, y que el dónde quebrar lo dicta el contenido, no el catálogo de dispositivos (web-fundamentals módulo 7). Y lo comprobaste ejecutando: el catálogo de Mercado pasó de 1 a 2 a 4 columnas usando tres puntos de la misma escala, con una sola cadena de grid.

Antes de avanzar deberías poder: explicar por qué una escala de breakpoints compartida da coherencia; personalizar screens en el config; distinguir cuándo un cambio pide un breakpoint (discreto) y cuándo una unidad fluida (continuo); y aplicar el criterio content-first para elegir dónde quebrar.

Con esto cierras la parte responsive del módulo. La lección 4 abre la otra dimensión: el tema. Vas a conocer la variante dark: —el prefijo que, en vez de reaccionar al ancho, reacciona al modo oscuro—. dark:bg-gray-900 es "usa este fondo cuando el tema sea oscuro", igual que md:p-4 era "usa este padding a partir de 768px". Es el mismo mecanismo de prefijos que ya dominas, aplicado a una condición distinta —el tema en vez del tamaño—, y el primer paso hacia el dark mode del sistema.

Recursos