Diseño responsive
En resumen: El diseño responsive hace que una página funcione en cualquier pantalla: la etiqueta meta viewport, mobile first y las media queries que cambian la disposición según el ancho.
Problema
El botón está fuera de la pantalla
La página del producto se hizo en un monitor, y allí se ve bien. Un cliente con un teléfono escribe: «No puedo pedir, no hay ningún botón». Aquí está con el ancho de un teléfono, 360px:
Consola
El botón «Pedir» está a la derecha, fuera de la pantalla. La página se desplaza hacia los lados, y a la mayoría no se le ocurre desplazarla. El código no tiene ningún error: la imagen mide 400px, la columna de texto 360px, y van una al lado de la otra. En un monitor de 1000px de ancho cabe. En 360px no.
La idea
Las pantallas tienen anchos distintos
La misma página se abre en un teléfono (360–430px), en una tableta (unos 800px) y en una computadora (1000px o más). Una página que se adapta a cualquier ancho se llama responsive.
El primer paso está en <head>:
<meta name="viewport" content="width=device-width, initial-scale=1">Sin ella, el teléfono se hace pasar por un monitor: coloca la página a 980px y la reduce hasta que cabe. El texto queda diminuto, y tu CSS nunca ve los 360px reales. Con ella, el ancho de la página es el ancho de la pantalla. En el sandbox añadimos esta línea por ti, pero en tus propias páginas la escribes tú.
El segundo paso es el orden en que escribes el CSS. Mobile first significa: las reglas base son para la pantalla más estrecha. Allí todo va uno debajo de otro, y eso casi siempre cabe. Luego añades cambios para las pantallas más anchas con una media query:
.product { display: grid; gap: 16px;}@media (min-width: 700px) { .product { grid-template-columns: 1fr 1fr; }}@media (min-width: 700px) es una condición: «el ancho es de al menos 700px». Las reglas de dentro solo se aplican si la condición es verdadera. Si no, el navegador se las salta.
El tercer paso son las imágenes. width="400" es bueno para el HTML: el navegador reserva el espacio mientras carga la imagen. Pero en una pantalla estrecha necesita un techo:
img { max-width: 100%; height: auto;}max-width: 100% significa «no más ancha que su caja», y height: auto conserva las proporciones para que no se deforme.
Míralo
Mira cómo se activan las consultas
La ventana crece de 360px a 1000px. Fíjate en la escala de arriba y en las líneas del código.
body { margin: 0; padding: 16px; }.cards { display: grid; grid-template-columns: 1fr; gap: 16px;}@media (min-width: 600px) { .cards { grid-template-columns: 1fr 1fr; }}@media (min-width: 900px) { .cards { grid-template-columns: repeat(4, 1fr); }}Paso a paso, en palabras
- Un teléfono, 360px. La regla base es para la pantalla más estrecha: una columna. Una tarjeta mide 360 − 2 × 16 = 328px.
- La ventana se ensancha y la columna crece con ella. A 599px la condición min-width: 600px todavía es falsa y el navegador se salta las reglas de dentro.
- A 600px la condición es verdadera. Su regla va después de la base y gana: dos columnas de 276px. Las tarjetas se reordenan.
- Aún más ancho: las dos columnas crecen hasta 425,5px. min-width: 900px todavía es falsa.
- A 900px las dos consultas son verdaderas. Fijan la misma propiedad, así que gana la última: cuatro columnas de 205px.
- A 1000px no se activa nada nuevo. Las cuatro columnas crecen hasta 230px.
- El navegador vuelve a comprobar las consultas con cada cambio de ancho: un teléfono girado, una ventana más estrecha. Si la estrechas, las columnas vuelven por el mismo camino.
Una media query no se calcula una sola vez. El navegador la comprueba con cada cambio de ancho y recoloca la página al instante. Las dos consultas fijan la misma propiedad con la misma especificidad, así que a 900px gana la última. Es la cascada que ya conoces.
Pruébalo
Arréglala para un teléfono
La página mide 360px de ancho. Hazla mobile first: la imagen y el texto uno debajo de otro, y nada que sobresalga por los lados. La imagen no es más ancha que su caja. A partir de 700px, la imagen y el texto van en dos columnas. La comprobación busca el desplazamiento lateral y en cuántas filas están la imagen y la columna de texto.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pan Dobrudzha · La tienda</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="product">
<img src="img/bread.svg" width="400" height="400" alt="Una hogaza redonda de pan blanco sobre una tabla">
<div class="info">
<h1>Pan Dobrudzha</h1>
<p>Pan blanco, 650 g, horneado esta mañana.</p>
<p class="price">1,80 €</p>
<button type="button">Pedir</button>
</div>
</main>
</body>
</html>
style.cssbody {
margin: 0;
font-family: system-ui, sans-serif;
}
.product {
display: grid;
gap: 16px;
padding: 16px;
}
img {
max-width: 100%;
height: auto;
}
.price {
font-size: 1.5rem;
font-weight: 700;
}
button {
background: #1a7f37;
color: #fff;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 1rem;
}
@media (min-width: 700px) {
.product {
grid-template-columns: 1fr 1fr;
gap: 32px;
}
}
.product pasa a display: grid con una columna. img { max-width: 100%; height: auto; } reduce la imagen al ancho de su caja. Las dos columnas están en @media (min-width: 700px), así que no se aplican en el teléfono. overflow-x: hidden en body no es una solución: el desplazamiento desaparece, pero el botón sigue fuera de la pantalla.
Pruébalo
Ahora para una computadora
Esta página ya está hecha para un teléfono: todo en una columna. Aquí el ancho es de 1000px. Añade una media query para que, a partir de 700px, la imagen y el texto vayan en dos columnas iguales.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pan Dobrudzha · La tienda</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="product">
<img src="img/bread.svg" width="400" height="400" alt="Una hogaza redonda de pan blanco sobre una tabla">
<div class="info">
<h1>Pan Dobrudzha</h1>
<p>Pan blanco, 650 g, horneado esta mañana.</p>
<p class="price">1,80 €</p>
<button type="button">Pedir</button>
</div>
</main>
</body>
</html>
style.cssbody {
margin: 0;
font-family: system-ui, sans-serif;
}
.product {
display: grid;
gap: 16px;
padding: 16px;
}
img {
max-width: 100%;
height: auto;
}
.price {
font-size: 1.5rem;
font-weight: 700;
}
button {
background: #1a7f37;
color: #fff;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 1rem;
}
@media (min-width: 700px) {
.product {
grid-template-columns: 1fr 1fr;
gap: 32px;
}
}
@media (min-width: 700px) { .product { grid-template-columns: 1fr 1fr; gap: 32px; }}max-width: 700px es lo contrario: «como máximo 700px». Se activa solo en pantallas estrechas. min-width: 700 sin px no es válido, y una consulta no válida nunca es verdadera.
Pruébalo
Encuentra el error
En una computadora debería haber dos columnas, pero la página se queda en una. ¿Por qué?
Elige la línea con el error.
La media query es verdadera, pero la regla base va después. Las dos son de .product, con la misma especificidad, y gana la última: grid-template-columns: 1fr de la línea 8. Con mobile first las reglas base van primero y las media queries después.
Pruébalo
Completa
¿Qué palabra hace que las dos columnas se apliquen a partir de 700px?
@media (: 700px) {
.product {
grid-template-columns: 1fr 1fr;
}
}min-width: «el ancho es de al menos 700px». Con mobile first, las consultas casi siempre usan min-width.
La idea
El color en un solo lugar
El verde de la tienda está en el CSS en cuatro sitios: el precio, los enlaces, el botón y el borde. La tienda cambia de color y alguien tiene que encontrarlos todos. Si se le escapa uno, la página queda con dos colores.
Una variable CSS (propiedad personalizada) es un valor con nombre. El nombre empieza con dos guiones, y var() lo lee:
:root { --accent: #1a7f37;}button { background: var(--accent);}:root es el elemento más alto, <html>. Las variables se heredan como color, así que lo que está en :root se ve en toda la página. El color cambia en una sola línea.
Lo mismo da un tema oscuro. @media (prefers-color-scheme: dark) es verdadera cuando alguien ha elegido un tema oscuro en su teléfono o computadora. Dentro solo cambias los valores de las variables. Las reglas que las usan siguen igual.
Míralo
Claro y oscuro
Consola
El sandbox sigue el tema de tu navegador, no el botón de tema de esta web. Cambia el tema de tu sistema a oscuro y pulsa «Ejecutar»: el fondo, el texto y el verde cambian, mientras que las reglas CSS de .card, .price y button son las mismas. El verde sobre oscuro es más claro, para que se lea sobre el fondo oscuro.
Pruébalo
Un color nuevo para la tienda
La tienda cambia el verde #1a7f37 por el naranja #c2410c. Crea una variable --accent en :root con el valor #c2410c y úsala en los cuatro sitios donde ahora está el verde.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pan Dobrudzha · La tienda</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<article class="card">
<h1>Pan Dobrudzha</h1>
<p class="price">1,80 €</p>
<p>Mira también <a href="/bread">otros panes</a></p>
<button type="button">Pedir</button>
</article>
</main>
</body>
</html>
style.css:root {
--accent: #c2410c;
}
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
}
.card {
max-width: 320px;
padding: 16px;
border-top: 4px solid var(--accent);
}
.price {
color: var(--accent);
font-size: 1.5rem;
font-weight: 700;
}
a {
color: var(--accent);
}
button {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 1rem;
}
:root { --accent: #c2410c;}Luego los cuatro sitios dicen var(--accent). La comprobación lee tanto la variable como los colores reales. Así que el naranja escrito a mano en cuatro sitios no pasa. Tampoco var(accent) sin los guiones: el navegador descarta esa declaración y el botón se queda gris.
La idea
Estados: señalado, enfocado, desactivado
Un botón no se ve de una sola manera. Puede estar bajo el ratón, enfocado con el teclado o desactivado. CSS los atrapa con pseudoclases: una palabra con dos puntos detrás del selector.
button:hover { background: #116329;}button:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px;}button:disabled { background: #9ca3af; cursor: not-allowed;}:hover: el ratón está sobre el elemento. Un teléfono no tiene ratón, así que no se esconde información importante tras:hover.:focus-visible: el elemento tiene el foco y el navegador decide que el foco debe verse. Con el teclado siempre se ve. Con un clic de ratón en un botón, normalmente no. Por esooutline: noneen:focuses un error: quien usa el teclado pierde de vista dónde está.:disabled: el elemento tiene el atributodisabled. El botón «Agotado» debe verse desactivado, no solo estarlo.
Y el tamaño. En un teléfono se pulsa con el dedo, que es menos preciso que un ratón. Un botón de 24px de alto se falla. Da a los botones una altura de al menos 44px: min-height: 44px.
Pruébalo
Los botones en un teléfono
Un compañero quitó el contorno del foco, y los botones son bajos. Haz que los botones tengan al menos 44px de alto (min-height); que con el foco del teclado haya un contorno continuo de 3px del color --accent; y que el botón desactivado «Agotado» sea gris (#9ca3af), con el cursor not-allowed. La comprobación pulsa Tab y mira el botón «Pedir».
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pan Dobrudzha · La tienda</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<article>
<h2>Pan Dobrudzha</h2>
<p>1,80 €</p>
<button type="button">Pedir</button>
</article>
<article>
<h2>Queso kashkaval</h2>
<p>7,50 €</p>
<button type="button" disabled>Agotado</button>
</article>
</main>
</body>
</html>
style.css:root {
--accent: #1a7f37;
}
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
}
button {
min-height: 44px;
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 8px 16px;
}
button:hover {
background: #116329;
}
button:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
Quita button:focus { outline: none; }. Si se queda después de :focus-visible, gana: la especificidad es la misma y va más tarde. El contorno va en button:focus-visible y el gris en button:disabled. La pseudoclase :disabled atrapa el atributo disabled del HTML, así que no hace falta una clase .disabled.
Más precisamente, para curiosos
El viewport, con más precisión. Sin la etiqueta meta, los navegadores móviles colocan la página a 980px y la reducen. En Chromium con un teléfono emulado de 360px, innerWidth es 980 y @media (min-width: 700px) es verdadera: el teléfono recibe la disposición de la computadora. initial-scale=1 significa «sin reducir al empezar». No desactives el zoom (user-scalable=no, maximum-scale=1): las personas con poca vista agrandan la página con dos dedos.
Puntos de ruptura (breakpoints). 600, 700, 900 no son números sagrados. Pon un punto de ruptura donde tu diseño empiece a verse mal, no en el modelo de algún teléfono. Algunos escriben las consultas en em (min-width: 40em): así también siguen el tamaño de letra que la persona haya elegido.
Consultas no solo de ancho. (hover: hover) es verdadera cuando el dispositivo señalador principal puede pasar por encima (un ratón), y (pointer: coarse) lo es para un dedo. (prefers-reduced-motion: reduce) dice que la persona no quiere movimiento: quita las animaciones. (orientation: landscape) es para una pantalla en horizontal.
Las variables, con más precisión. var(--accent, #1a7f37) tiene un valor de reserva por si falta la variable. Una variable puede cambiar en una parte de la página: .sale { --accent: #c2410c; } y todo lo de .sale es naranja. El valor se comprueba solo cuando se usa: --accent: 12px está permitido, pero entonces color: var(--accent) no es válido y el color pasa a ser el heredado. La propiedad color-scheme: light dark le dice al navegador que pinte también sus propias partes (campos, barras de desplazamiento) con el tema oscuro.
Tamaño para el dedo. WCAG 2.2 (criterio 2.5.8) pide un objetivo de al menos 24 × 24px o suficiente espacio alrededor. Apple recomienda 44 × 44pt y Material Design 48 × 48dp.
Ejercicio
El catálogo en un teléfono
El catálogo se hizo para una computadora: cuatro tarjetas de 220px en una fila. Aquí el ancho es de 360px. Hazlo para un teléfono: las tarjetas en dos columnas, sin desplazamiento lateral, imágenes no más anchas que su tarjeta, los botones «Comprar» de al menos 44px de alto (min-height). Pon el verde #1a7f37 en una variable --accent en :root y haz que los botones la usen. Si quieres, añade también cuatro columnas a partir de 700px.
Consola
Solución
index.html<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Catálogo · La tienda</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<h1>Catálogo</h1>
<div class="cards">
<article class="card">
<img src="img/bread.svg" width="220" height="220" alt="">
<h2>Pan Dobrudzha</h2>
<p>1,80 €</p>
<button type="button">Comprar</button>
</article>
<article class="card">
<img src="img/milk.svg" width="220" height="220" alt="">
<h2>Leche fresca 3 %</h2>
<p>2,40 €</p>
<button type="button">Comprar</button>
</article>
<article class="card">
<img src="img/cheese.svg" width="220" height="220" alt="">
<h2>Queso kashkaval</h2>
<p>7,50 €</p>
<button type="button">Comprar</button>
</article>
<article class="card">
<img src="img/apples.svg" width="220" height="220" alt="">
<h2>Manzanas, 1 kg</h2>
<p>2,20 €</p>
<button type="button">Comprar</button>
</article>
</div>
</main>
</body>
</html>
style.css:root {
--accent: #1a7f37;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
}
main {
padding: 16px;
}
.cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.card img {
max-width: 100%;
height: auto;
}
.card button {
min-height: 44px;
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 8px 16px;
}
@media (min-width: 700px) {
.cards {
grid-template-columns: repeat(4, 1fr);
}
}
grid-template-columns: 1fr 1fr divide el ancho en dos, mientras que repeat(2, 220px) vuelve a ser fijo y no cabe: dos tarjetas y el gap son 2 × 220 + 16 = 456px, más de 360px. Las imágenes necesitan max-width: 100%; si no, con width: 220px estiran las columnas. Las cuatro columnas para la computadora van en @media (min-width: 700px) después de la regla base. La comprobación se hace en el teléfono y no las ve, pero con ellas la página está completa.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Selectores y la cascada · El modelo de caja · Flexbox: fila y columna · Grid: filas y columnas
- Ejercicios sobre el tema: CSS: el aspecto de la página