learning-front

Nivel 1 · HTML y CSS: la estructura y la piel

Responsive y mobile-first sin frameworks

Media queries y unidades fluidas para que tu web funcione en cualquier pantalla, entendiendo el fundamento que automatizan Bootstrap y Tailwind.

El HTML que escribes tiene que funcionar en el móvil de quien lo consulta en el metro, en el escritorio de quien trabaja con dos monitores y en todo lo que hay en medio. En 2026 más de la mitad del tráfico web viene de móvil. No es un caso extremo a contemplar al final: es el punto de partida.

En los capítulos anteriores aprendiste las unidades relativas (rem, em, %, vw), montaste el layout con Flexbox y organizaste el grid del Team Builder con CSS Grid y custom properties. Ahora el objetivo es que ese grid funcione en cualquier tamaño de pantalla: del móvil al escritorio, sin romper nada.

Por qué mobile-first y no al revés#

Hay dos formas de hacer un diseño responsive. La más intuitiva es diseñar para escritorio y luego reducir con max-width:

css
/* Desktop-first: parto de tres columnas y las reduzco */
.hero-grid { grid-template-columns: repeat(3, 1fr); }

@media (max-width: 768px) {
  .hero-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  .hero-grid { grid-template-columns: 1fr; }
}

La otra forma es empezar por la pantalla más pequeña y ampliar con min-width:

css
/* Mobile-first: parto de una columna y añado */
.hero-grid { grid-template-columns: 1fr; }

@media (min-width: 37.5em) {
  .hero-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 56.25em) {
  .hero-grid { grid-template-columns: repeat(3, 1fr); }
}

El resultado visual es el mismo. La diferencia está en el razonamiento. Mobile-first parte de lo simple —un grid de una columna— y añade complejidad para pantallas que tienen más espacio. Desktop-first parte de la versión compleja y la parcela hacia abajo. Construir desde lo simple es más sano: los casos base son más fáciles de razonar y los parches se acumulan menos.

Además, hay un motivo práctico. El móvil descarga la misma hoja de estilos en los dos casos —no hay descarga selectiva—, pero el trabajo que hace con ella cambia. Si la CSS va de mayor a menor, el móvil aplica primero los estilos de escritorio y luego los sobreescribe con los suyos. Con mobile-first, los bloques de escritorio van dentro de media queries min-width que en el móvil ni se activan, así que se ahorra ese repintado. La diferencia en rendimiento hoy es mínima, pero el principio cuenta.

El meta viewport: imprescindible#

Antes de que funcione cualquier media query en un móvil real, necesitas esta etiqueta en el <head>:

html
<!-- width=device-width: usa el ancho real del dispositivo; initial-scale=1: sin zoom inicial -->
<meta name="viewport" content="width=device-width, initial-scale=1" />

Sin ella, los navegadores móviles simulan un viewport de ~980px de ancho y reducen la página para que quepa en la pantalla. Tus media queries se activan como si el viewport fuera ancho de escritorio, no el ancho real del dispositivo. Con esta etiqueta, el viewport coincide con el ancho físico del teléfono y todo funciona como esperas.

Unidades relativas en breakpoints#

Como viste en «Color, unidades y tipografía», rem respeta la fuente base del usuario y em es relativo al elemento actual. Aquí hay un matiz concreto para las media queries: conviene usar em en los breakpoints en lugar de px.

La razón: si el usuario amplía la fuente del navegador, los puntos de ruptura también se ajustan. 37.5em son 600px con fuente de 16px, pero 750px con fuente de 20px. El layout salta a dos columnas cuando hay espacio real para dos columnas —no cuando hay 600px independientemente de cuánto ocupa el texto—.

css
/* Con em: el breakpoint se adapta a la fuente del usuario */
/* 600px en condiciones normales */
@media (min-width: 37.5em) {
  .hero-grid { grid-template-columns: repeat(2, 1fr); }
}

clamp(): valores fluidos sin media queries#

clamp(mínimo, preferido, máximo) devuelve el valor preferido recortado para que nunca salga de los límites. Se usa donde quieres que algo crezca con el viewport sin saltos:

css
.page {
  /* En móvil (~320px): 1rem. En escritorio (~1200px): 2.5rem.
     Entre medias, crece de forma continua. */
  padding: clamp(1rem, 5vw, 2.5rem);
}

h1 {
  /* El título crece de 1.5rem a 2.5rem según el ancho del viewport. */
  font-size: clamp(1.5rem, 4vw, 2.5rem);
}

El truco es el valor central (5vw, 4vw). Como vw escala con el viewport, el padding crece de forma continua. El mínimo y el máximo ponen el límite para que no quede ni demasiado apretado ni demasiado grande. Cero media queries para eso.

El grid que se adapta solo: auto-fill y minmax()#

En el capítulo anterior ya usaste auto-fill y minmax para el grid del Team Builder. Aquí el foco es la dimensión responsive: por qué ese patrón elimina la necesidad de media queries para el número de columnas:

css
.hero-grid {
  /* activa la cuadrícula */
  display: grid;
  /* tantas columnas como quepan, cada una de 15rem como mínimo */
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  /* separación entre celdas */
  gap: 1rem;
}

auto-fill le dice al navegador que cree tantas columnas como quepan en el contenedor. minmax(15rem, 1fr) define que cada columna tiene al menos 15rem y puede crecer hasta 1fr para llenar el espacio. El resultado:

  • Contenedor de 30rem: caben 2 columnas de 15rem. El grid tiene 2 columnas.
  • Contenedor de 48rem: caben 3 columnas de 15rem. El grid tiene 3 columnas.
  • Contenedor de 20rem: cabe 1 columna. El grid tiene 1 columna.

No hay @media. No hay breakpoints. El navegador hace la aritmética. Y si mañana añades más héroes o la pantalla tiene 3000px, el layout se adapta sin tocar el CSS.

El 15rem no es arbitrario: es el ancho mínimo para que la tarjeta de héroe sea legible. Eso tiene sentido semántico. Un 320px suelto no lo tiene.

Imágenes que no se deforman#

Una imagen con tamaño fijo rompe el responsive: si la pantalla es más estrecha que la imagen, se sale. La base es hacerla fluida:

css
img {
  /* la imagen nunca supera el ancho de su contenedor */
  max-width: 100%;
  /* el alto se ajusta solo para conservar la proporción */
  height: auto;
}

Así nunca supera el ancho de su contenedor y conserva su proporción. Pero a veces quieres que una imagen rellene una caja de tamaño fijo —un avatar cuadrado, una portada— sin deformarse. Ahí entran dos propiedades:

css
.portada {
  /* la caja mantiene esta proporción a cualquier ancho */
  aspect-ratio: 16 / 9;
  /* la imagen cubre la caja y recorta lo que sobra */
  object-fit: cover;
}

aspect-ratio fija la proporción de la caja y reserva su espacio antes de que la imagen cargue, evitando saltos de layout. object-fit: cover hace que la imagen cubra toda la caja manteniendo su proporción, recortando lo que no cabe; contain, en cambio, la encaja entera dejando hueco. Sin object-fit, una imagen estirada a una caja de otra proporción se ve deformada.

Eso resuelve el tamaño en pantalla. Hay un segundo frente que no entra en este capítulo pero conviene que sepas que existe: la resolución del fichero. Con srcset y sizes en el <img> —o con el elemento <picture>— le das al navegador varias versiones de la misma imagen (una ligera para móvil, una grande para pantallas densas) y él elige la que toca. Es el estándar para imágenes responsive de verdad y lo verás cuando entres en optimización; por ahora, max-width: 100% ya evita que se deformen.

min() y max(): límites puntuales#

clamp() es para valores fluidos con dos límites. A veces solo necesitas uno:

css
/* El contenedor nunca supera 60rem, pero si la pantalla es más estrecha, se adapta */
.page { width: min(60rem, 100%); }

/* El título nunca es menor de 1rem, aunque el viewport sea muy estrecho */
.titulo-hero { font-size: max(1rem, 2vw); }

Son más puntuales que clamp(), pero el principio es el mismo: definir el comportamiento mediante relaciones, no mediante tamaños fijos.

Una nota sobre la altura del viewport en móvil#

Una pieza moderna que evita un dolor de cabeza clásico. En móvil, 100vh (el 100% del alto de la ventana) cuenta también la franja que tapa la barra del navegador, así que un bloque pensado a pantalla completa se desborda un poco y aparece scroll donde no lo quieres. La unidad dvh (dynamic viewport height, alto dinámico) mide el viewport realmente visible y se ajusta cuando esa barra aparece o desaparece; sus parientes svh y lvh miden el viewport más pequeño y el más grande. No te hacen falta para el ejercicio, pero cuando veas un 100vh dando problemas en un móvil, ya sabes la alternativa: 100dvh.

Una nota sobre los frameworks#

Bootstrap, Tailwind y similares automatizan exactamente lo que acabas de aprender. Sus clases sm:, md:, lg: o col-md-4 son azúcar sobre media queries y grids como los que has escrito aquí. Usarlos sin entender el fundamento lleva a layouts que se rompen cuando el caso de uso se sale de lo previsto. Usarlos conociendo el fundamento te da control sobre lo que generan y criterio para depurar cuando algo no funciona.

Pruébalo#

El grid del Team Builder con auto-fill + minmax(): sin una sola media query, el número de columnas se adapta al espacio disponible. Arrastra la manija del borde derecho del preview (o usa los presets Móvil / Tablet / Escritorio) y mira cómo el grid recoloca las columnas a cada ancho, sin tocar el CSS. Después prueba a cambiar 12rem en styles.css por 8rem o por 20rem y observa cómo cambia el punto en el que entra o sale una columna.

Comprueba lo que sabes#

Pregunta 1 de 5

En un enfoque mobile-first, ¿qué va en la regla CSS base (fuera de cualquier media query)?

Tu turno#

El HTML ya está montado. Tu tarea es el CSS: hacer que el grid sea responsive. Abre styles.css en el playground, sigue los TODOs y aplica lo que has aprendido en este capítulo. Arrastra el preview o usa los presets de dispositivo para comprobar tu CSS a distintos anchos: ahí verás tus media queries dispararse de verdad. Empieza con lo que sepas y sube de nivel. Cuando creas que lo tienes, despliega las soluciones y compara tu enfoque con los tres niveles.

Ejercicio · en esta página

Haz responsive el grid del Team Builder

El HTML del Team Builder ya está montado y es semántico. Tu trabajo es el CSS: hacer que el grid de héroes se adapte a cualquier ancho de pantalla. El starter te da el marcado completo y un styles.css con TODOs. Empieza con lo que sepas (columnas fijas, media queries) y sube de nivel con lo que hayas aprendido en este capítulo.

Paso 1: Que funcione

  • El grid se ve en escritorio con varias columnas.
  • En móvil no hay scroll horizontal.
  • Hay media queries con max-width para reducir columnas al estrechar la pantalla.
Ver soluciones
/*
  NIVEL OK — desktop-first con max-width y anchos fijos

  Funciona. En escritorio se ven tres columnas y en móvil no se rompe del todo.
  Pero el enfoque está invertido: se diseña para escritorio y se parchan los
  tamaños pequeños con media queries descendentes (max-width).

  Problemas que hereda este enfoque:
  - La base ya tiene ancho fijo (320px por columna). Si la pantalla es más
    estrecha, el navegador hace scroll horizontal o aplasta el contenido.
  - Cada breakpoint añade un parche sobre el anterior en lugar de construir
    desde lo sencillo hacia lo complejo.
  - Los tamaños en px son "números mágicos": ¿por qué 320? ¿Y si la fuente
    base cambia? No escalan con nada.
*/

/* ─── Variables y reset ──────────────────────────────────────── */

:root {
  color-scheme: light;
  --tinta: #1c1b22;
  --tinta-suave: #5b5966;
  --linea: #e6e3ee;
  --fondo: #f7f6fb;
  --tarjeta: #ffffff;
  --acento: #b8336a;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--tinta);
  background: var(--fondo);
  line-height: 1.5;
}

/* ─── Contenedor de página ───────────────────────────────────── */

.page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 40px 24px 64px;
}

/* ─── Cabecera ───────────────────────────────────────────────── */

.site-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--linea);
  margin-bottom: 2rem;
}
.brand {
  font-size: 1.4rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0;
}
.brand span {
  color: var(--acento);
}
.site-nav a {
  color: var(--tinta-suave);
  text-decoration: none;
  font-size: 0.9rem;
  margin-left: 1rem;
}
.site-nav a:hover {
  color: var(--acento);
}

/* ─── Título de sección ──────────────────────────────────────── */

.section-title {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--tinta-suave);
  margin: 0 0 1rem;
}

/* ─── Grid de héroes — desktop-first ────────────────────────── */

/* Base: tres columnas fijas. El número 320px es arbitrario. */
.hero-grid {
  display: grid;
  grid-template-columns: 320px 320px 320px;
  gap: 16px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Parche para tablet: dos columnas cuando no caben tres. */
@media (max-width: 1024px) {
  .hero-grid {
    grid-template-columns: 320px 320px;
  }
}

/* Parche para móvil: una columna cuando no cabe ninguna. */
@media (max-width: 680px) {
  .hero-grid {
    grid-template-columns: 1fr;
  }
}

/* ─── Tarjeta ────────────────────────────────────────────────── */

.hero-card {
  background: var(--tarjeta);
  border: 1px solid var(--linea);
  border-radius: 12px;
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.hero-top {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}
.hero-portrait {
  flex: none;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-weight: 700;
  color: #fff;
  background: var(--acento);
}
.hero-name {
  font-size: 1.05rem;
  font-weight: 700;
  margin: 0;
}
.hero-role {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--tinta-suave);
  margin: 0.1rem 0 0;
}
.hero-stats {
  display: flex;
  gap: 1.25rem;
  margin: 0;
  padding-top: 0.5rem;
  border-top: 1px solid var(--linea);
}
.stat {
  display: flex;
  flex-direction: column;
}
.stat dt {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--tinta-suave);
  margin: 0;
}
.stat dd {
  font-size: 1.05rem;
  font-weight: 600;
  margin: 0.15rem 0 0;
}

/* ─── Pie ────────────────────────────────────────────────────── */

.site-footer {
  margin-top: 3rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--linea);
  color: var(--tinta-suave);
  font-size: 0.85rem;
}

Por qué este nivel

  • Resuelve el responsive con columnas fijas en px y media queries de max-width.
  • El enfoque es desktop-first: se diseña para escritorio y se parchea hacia abajo.
  • Funciona. Pero los 320px son arbitrarios y no escalan si cambia la fuente base.
  • Al estrechar la pantalla, el layout aguanta a base de parches, no de un diseño pensado desde el principio para pantallas pequeñas.