Responsive design
Más del 60% del tráfico web viene de móviles. Si tu web no se ve bien en un iPhone, pierdes a más de la mitad de tus usuarios. Responsive design no es un extra — es lo mínimo. En esta lección vas a aprender cómo hacer que un layout se adapte a cualquier pantalla, desde un reloj hasta un ultrawide de 34".
El meta viewport
Sin esta línea en tu <head>, nada de responsive funciona:
<meta name="viewport" content="width=device-width, initial-scale=1">
Le dice al navegador móvil: "usa el ancho real del dispositivo, no simules ser un monitor de escritorio". Si no lo pones, el móvil renderiza la página a 980px y la encoge. Inusable.
Mobile first: diseña para móvil primero
La estrategia mobile first significa escribir los estilos base para móvil y luego añadir complejidad para pantallas más grandes con min-width:
/* Base: móvil (una columna) */
.card-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
/* Tablet: 2 columnas */
@media (min-width: 640px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
padding: 2rem;
}
}
/* Desktop: 3 columnas */
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
¿Por qué mobile first y no al revés?
- Los estilos móviles son más simples (una columna, sin sidebar). Es más fácil empezar simple y añadir.
- Si el CSS no carga, el usuario móvil tiene una experiencia funcional.
- Obliga a priorizar el contenido: ¿qué es lo más importante?
Media queries
Las media queries aplican estilos solo cuando se cumple una condición:
/* Por ancho de pantalla */
@media (min-width: 768px) { /* tablets y superior */ }
@media (min-width: 1024px) { /* desktop */ }
@media (min-width: 1280px) { /* desktop grande */ }
/* Por preferencia del usuario */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0a0a0b;
--color-text: #e0e0e0;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* Por orientación */
@media (orientation: landscape) {
.hero { min-height: 100dvh; }
}
/* Combinar condiciones */
@media (min-width: 768px) and (max-width: 1023px) {
/* Solo tablets */
}
prefers-reduced-motiones crítica para accesibilidad. Hay personas con trastornos vestibulares para quienes las animaciones causan mareo real. Siempre respétala.
Unidades relativas
Las unidades fijas (px) no se adaptan. Las relativas sí:
| Unidad | Relativa a | Uso típico |
|---|---|---|
rem |
font-size del <html> |
Tamaños de texto, spacing, padding |
em |
font-size del elemento padre | Márgenes relativos al texto |
% |
Tamaño del contenedor padre | Anchos fluidos |
vw / vh |
Viewport (ventana del navegador) | Heroes, secciones a pantalla completa |
dvh |
Viewport dinámico | Altura real en móvil (sin barra de navegación) |
ch |
Ancho del carácter "0" | Ancho de lectura óptimo (65ch) |
Viewport units: vh vs dvh vs svh vs lvh
.hero {
/* vh: puede dejar espacio o solaparse con la barra del navegador en móvil */
min-height: 100vh;
/* dvh: se ajusta dinámicamente cuando la barra del navegador aparece/desaparece */
min-height: 100dvh; /* ✅ Usa esta */
/* svh: la altura más pequeña posible (con barra visible) */
min-height: 100svh;
/* lvh: la altura más grande posible (sin barra visible) */
min-height: 100lvh;
}
En móvil, la barra de dirección del navegador aparece y desaparece al hacer scroll. vh no tiene esto en cuenta, lo que causa un bug clásico donde el hero "rebota". dvh lo resuelve.
clamp(): valores fluidos sin media queries
.hero-title {
/* clamp(mínimo, preferido, máximo) */
font-size: clamp(2rem, 5vw, 4.5rem);
/* En móvil: 2rem, crece con el viewport, máximo 4.5rem */
}
.container {
width: clamp(320px, 90%, 1200px);
margin-inline: auto;
}
.card {
padding: clamp(1rem, 3vw, 2.5rem);
}
clamp() es una de las funciones más útiles de CSS moderno. Un valor que se adapta al viewport sin escribir una sola media query.
Container queries: responsive basado en el componente
Las media queries miran el viewport (la ventana del navegador). Pero ¿y si una card está en un sidebar estrecho en desktop? Media queries no lo detectan. Container queries sí:
/* Definir el contenedor */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Estilar según el tamaño del CONTENEDOR, no del viewport */
@container card (min-width: 400px) {
.card {
display: flex;
gap: 1.5rem;
}
.card-image {
flex: 0 0 150px;
}
}
@container card (max-width: 399px) {
.card {
display: block;
}
.card-image {
width: 100%;
}
}
<!-- La misma card se adapta al espacio disponible -->
<div class="card-wrapper" style="width: 600px">
<article class="card"><!-- Layout horizontal --></article>
</div>
<div class="card-wrapper" style="width: 280px">
<article class="card"><!-- Layout vertical --></article>
</div>
Container queries hacen que los componentes sean verdaderamente reutilizables: se adaptan al espacio donde los pongas, no a la pantalla global.
Container query units
.card-wrapper {
container-type: inline-size;
}
.card-title {
font-size: clamp(1rem, 4cqi, 1.5rem); /* cqi = 1% del inline-size del container */
}
Imágenes responsive
/* Regla básica: las imágenes nunca desbordan su contenedor */
img {
max-width: 100%;
height: auto;
display: block;
}
/* Imagen que llena su contenedor (como background pero mejor) */
.card-image img {
width: 100%;
height: 200px;
object-fit: cover; /* Recorta para llenar sin deformar */
object-position: center;
}
.avatar img {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
Un patrón responsive completo
/* === Base: móvil === */
.page {
display: grid;
grid-template-areas:
"header"
"main"
"footer";
min-height: 100dvh;
}
.header { grid-area: header; }
.main { grid-area: main; }
.sidebar { display: none; } /* Oculto en móvil */
.footer { grid-area: footer; }
.container {
width: clamp(320px, 90%, 1200px);
margin-inline: auto;
padding-inline: 1rem;
}
.card-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* === Tablet === */
@media (min-width: 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.container {
padding-inline: 2rem;
}
}
/* === Desktop === */
@media (min-width: 1024px) {
.page {
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 280px 1fr;
}
.sidebar {
display: block;
grid-area: sidebar;
}
.card-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
}
Haz responsive la tienda de vinilos
Toma el layout de Vinyl Paradise y hazlo completamente responsive:
- Mobile first: todo en una columna por defecto
- La navbar: en móvil, logo arriba y links debajo (
flex-direction: column). En desktop, en fila conspace-between - Cards: 1 columna en móvil, 2 en tablet (640px), 3+ en desktop con
auto-fit - Título hero con
clamp()que escale fluidamente - Al menos una card con container query que cambie de layout vertical a horizontal cuando el contenedor es ancho
- Imágenes con
max-width: 100%yobject-fit: cover min-height: 100dvhen el hero (novh)- Respeta
prefers-reduced-motionsi tienes hover effects
lightbulb Pistas
Empieza escribiendo todo el CSS sin media queries — ese es tu layout móvil. Luego añade un breakpoint a 640px para tablet y otro a 1024px para desktop. Para la container query, envuelve la card en un div con container-type: inline-size.