StackPractices
intermediate Por Mathias Paulenko

Diseño Mobile Responsive

Construye layouts responsive que funcionan en cualquier dispositivo. Cubre CSS Grid, Flexbox, container queries, fluid typography, mobile-first breakpoints y responsive images.

Introducción

Diseño responsive significa construir layouts que se adaptan a cualquier tamaño de pantalla — desde teléfonos de 320px hasta monitores 4K. CSS moderno nos da Grid, Flexbox, container queries y fluid typography para construir interfaces responsive sin JavaScript. A continuación: estrategia mobile-first, layouts con CSS Grid, patrones de Flexbox, container queries, fluid typography, responsive images y estrategias de testing.

Estrategia Mobile-First

Empezar con la pantalla más pequeña y progresivamente enhancar para pantallas más grandes. Esto te fuerza a priorizar contenido y mantiene el CSS lean.

/* Base styles — mobile first */
.card {
  padding: 1rem;
  font-size: 0.875rem;
}

/* Tablet y superior */
@media (min-width: 768px) {
  .card {
    padding: 1.5rem;
    font-size: 1rem;
  }
}

/* Desktop y superior */
@media (min-width: 1024px) {
  .card {
    padding: 2rem;
    font-size: 1.125rem;
  }
}

Breakpoints comunes

/* Breakpoints inspirados en Tailwind */
/* sm:  640px */
/* md:  768px */
/* lg:  1024px */
/* xl:  1280px */
/* 2xl: 1536px */

/* Usar min-width (mobile-first), no max-width (desktop-first) */

Layouts con CSS Grid

Grid básico

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}

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

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

Auto-fit grid (sin media queries)

.auto-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

Holy grail layout

.layout {
  display: grid;
  grid-template-areas:
    "header header header"
    "nav    main   aside"
    "footer footer footer";
  grid-template-columns: 200px 1fr 250px;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}

@media (max-width: 768px) {
  .layout {
    grid-template-areas:
      "header"
      "main"
      "nav"
      "aside"
      "footer";
    grid-template-columns: 1fr;
  }
}

.header { grid-area: header; }
.nav    { grid-area: nav; }
.main   { grid-area: main; }
.aside  { grid-area: aside; }
.footer { grid-area: footer; }

Grid con named lines

.grid {
  display: grid;
  grid-template-columns: [sidebar-start] 250px [sidebar-end content-start] 1fr [content-end];
  grid-template-rows: [header-start] 80px [header-end body-start] 1fr [body-end];
}

.header  { grid-column: sidebar-start / content-end; grid-row: header-start / header-end; }
.sidebar { grid-column: sidebar-start / sidebar-end; grid-row: body-start / body-end; }
.content { grid-column: content-start / content-end; grid-row: body-start / body-end; }

Patrones de Flexbox

Centrado

.center {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main {
  flex: 1;
}
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 1.5rem;
}

.nav-links {
  display: flex;
  gap: 1.5rem;
}

@media (max-width: 768px) {
  .nav-links {
    display: none;
  }
  .nav-menu-toggle {
    display: block;
  }
}

Card con contenido flexible

.card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.card-content {
  flex: 1;
}

.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

Container Queries

Container queries permiten a los componentes responder al tamaño de su container, no del viewport. Esto habilita responsiveness a nivel componente real.

/* Definir un contexto de containment */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* Query el tamaño del container */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 1.5rem;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }
}

Container query units

.sidebar {
  /* cqw = container query width */
  font-size: clamp(0.875rem, 3cqw, 1.25rem);
  padding: 2cqi;
}

Fluid Typography

clamp()

/* min, preferred (viewport-relative), max */
h1 {
  font-size: clamp(1.5rem, 5vw, 3.5rem);
}

h2 {
  font-size: clamp(1.25rem, 4vw, 2.5rem);
}

p {
  font-size: clamp(1rem, 2.5vw, 1.125rem);
}

Fluid spacing

.section {
  padding: clamp(1rem, 5vw, 4rem);
  margin-bottom: clamp(1.5rem, 4vw, 3rem);
}

Responsive Images

srcset y sizes

<img
  src="image-800.jpg"
  srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w, image-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
  alt="Description"
  loading="lazy"
  decoding="async"
/>

picture element (art direction)

<picture>
  <source media="(max-width: 600px)" srcset="mobile.jpg" />
  <source media="(max-width: 1200px)" srcset="tablet.jpg" />
  <img src="desktop.jpg" alt="Description" />
</picture>

Aspect ratio

.image-container {
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.image-container img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

Responsive Tables

.table-wrapper {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

table {
  width: 100%;
  border-collapse: collapse;
  min-width: 600px;
}

/* Card layout en mobile */
@media (max-width: 640px) {
  table, thead, tbody, th, td, tr {
    display: block;
  }

  thead {
    display: none;
  }

  tr {
    margin-bottom: 1rem;
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
    padding: 0.75rem;
  }

  td {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0;
  }

  td::before {
    content: attr(data-label);
    font-weight: 600;
    margin-right: 1rem;
  }
}

Testing

Browser DevTools

  • Chrome DevTools: Device Mode (Ctrl+Shift+M)
  • Firefox: Responsive Design Mode (Ctrl+Shift+M)
  • Safari: Responsive Design Mode (Cmd+Ctrl+R)

CSS para testing

/* Debug grid lines */
* {
  outline: 1px solid red;
}

/* Mostrar container query boundaries */
@container card (min-width: 400px) {
  .card {
    outline: 2px dashed blue;
  }
}

Pautas

  • Empezar mobile-first — los base styles apuntan a la pantalla más pequeña, min-width media queries enhancan hacia arriba
  • Usar auto-fit grids — eliminar media queries para card layouts
  • Preferir clamp() para tipografía — scaling smooth sin saltos de breakpoint
  • Setear aspect-ratio en imágenes — prevenir layout shift (CLS)
  • Usar srcset para imágenes — servir la resolución apropiada por dispositivo
  • Añadir loading="lazy" — deferir imágenes off-screen
  • Usar container queries para componentes — desacoplar del viewport
  • Testear en dispositivos reales — los emuladores miss touch behavior y rendering bugs
  • Evitar fixed pixel widths — usar %, fr, vw, clamp() en su lugar
  • Manejar overflow explícitamenteoverflow-x: auto en tablas y code blocks
  • Usar gap en lugar de margins — spacing más limpio en contextos flex/grid
  • Setear min-width: 0 en flex children — prevenir overflow en nested flex

Errores Comunes

  • Usar max-width media queries (desktop-first) — los overrides son más difíciles y el CSS es más grande
  • Fixed pixel widths en containers — rompe en pantallas más pequeñas
  • No setear dimensiones de imagen — causa layout shift
  • Usar display: none para navegación mobile sin toggle — los usuarios no pueden navegar
  • Olvidar overflow-x: auto en tablas — horizontal scroll rompe la página
  • No testear en dispositivos reales — los emuladores miss performance y touch issues
  • Usar vh units sin fallback — el mobile browser chrome cambia el viewport height
  • Overusar media queries — container queries y auto-fit grids reducen la necesidad
  • No setear min-width: 0 en flex children — el contenido overflow los containers
  • Ignorar landscape orientation — los teléfonos en landscape tienen constraints diferentes

See Also

Preguntas frecuentes

¿Debo usar container queries o media queries?

Usar media queries para layout a nivel página (header, sidebar, main grid). Usar container queries para responsiveness a nivel componente (cards, widgets, sidebars que aparecen en diferentes contexts). Son complementarios — no un reemplazo.

¿Cuál es la diferencia entre auto-fit y auto-fill en CSS Grid?

auto-fit colapsa los tracks vacíos a zero, stretchando los items restantes para llenar la row. auto-fill preserva los tracks vacíos como gaps. Usar auto-fit cuando quieres que los items crezcan y llenen el espacio available. Usar auto-fill cuando quieres que los items mantengan su size y dejen gaps.

¿Cómo manejo el issue de mobile viewport height?

Los mobile browsers dinámicamente muestran/ocultan la address bar, cambiando vh. Usar 100dvh (dynamic viewport height) en lugar de 100vh. Para soporte de browsers más viejos, usar el viewport meta tag con interactive-widget=resizes-content o JavaScript con window.innerHeight.