Apuntes DAM
Volver al inicio

Landing de una cafetería: Flexbox, Grid y diseño adaptable

Ejercicio de HTML y CSSDifícilUnos 90 minutos

Maqueta paso a paso la página de una cafetería: la cabecera con Flexbox que se reorganiza en el móvil, la carta en una rejilla de 1, 2 o 3 columnas, variables y tipografía fluida con clamp, la etiqueta de agotado con ::after y un formulario de reservas accesible.

  • Flexbox
  • CSS Grid
  • Media queries (mobile first)
  • Variables CSS
  • clamp() y tipografía fluida
  • Pseudoelementos ::after
  • Formularios accesibles

Enunciado

Café Molino abre su web y el diseñador ya ha entregado el HTML de la página de inicio: una cabecera con el logo y el menú, una portada con el titular y el botón de reservas, y la carta con los platos. Falta todo el CSS, y el formulario de reservas lo hizo un becario sin pensar en la accesibilidad.

La mayoría de las visitas llegarán desde el móvil, así que el diseño se piensa primero para pantallas estrechas y se amplía con media queries (mobile first). Las pruebas de cada apartado comprueban la página a varios anchos, como lo haría un cliente al girar el móvil o abrirla en el ordenador.

Los apartados construyen la misma hoja de estilos poco a poco: cada uno empieza con lo hecho en el anterior y añade una parte nueva.

Qué hay que hacer

  1. Cada apartado se corrige por separado con pruebas que se ejecutan dentro de la página (algunas a varios anchos de pantalla). Debajo del editor tienes el resultado en directo.
  2. Los apartados de CSS empiezan con la solución del anterior ya escrita: así la página va creciendo. Puedes hacer los apartados en cualquier orden, pero conviene seguirlo.
  3. Respeta los nombres de las clases del HTML y los valores que pide cada apartado (separaciones, colores, tamaños): las pruebas miden la página.

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Flexbox para una dimensión, Grid para dos

La cabecera es una fila de elementos (una dimensión): Flexbox. La carta es una rejilla de filas y columnas (dos dimensiones): Grid. Elegir bien la herramienta simplifica mucho el CSS.

2. Mobile first

Escribe primero los estilos del móvil (sin media queries) y añade lo de las pantallas grandes con @media (min-width: …). Al revés funciona, pero obliga a deshacer estilos.

css
.carta { display: grid; gap: 20px; }
@media (min-width: 600px) { .carta { grid-template-columns: repeat(2, 1fr); } }
3. Mide con las herramientas del navegador

Si una prueba de medidas falla, abre la página en el navegador con las herramientas de desarrollo (F12), activa el modo de dispositivo y mira las cajas: el panel de Flexbox y Grid dibuja las líneas y las separaciones.

4. Valores que se calculan

clamp() y las variables hacen que el CSS se adapte solo: el título no necesita media queries para crecer, y el color de marca se cambia en un único sitio.

5. HTML accesible primero

Antes de dar estilo a un formulario, asegúrate de que funciona sin CSS: etiquetas, tipos de campo y un botón real. Pruébalo con el teclado (Tab, Espacio, Intro) y con el lector de pantalla del sistema.

Resuélvelo aquí

Cada apartado edita un fichero de la página (el HTML, el CSS o el JavaScript) y se corrige con pruebas que se ejecutan en la propia página, también a distintos anchos de pantalla. Debajo del editor ves el resultado en directo.

CSSApartado 1 · CSSDifícil

La cabecera: con Flexbox, el logo a la izquierda y el menú a la derecha en la misma línea, centrados en vertical, con 16px de relleno arriba y abajo y 24px a los lados; los enlaces del menú en fila con 24px de separación. En pantallas de menos de 600px, el menú pasa debajo del logo.

index.htmlsolo lectura
estilos.css
Vista previa
CSSApartado 2 · CSSDifícil

La carta (.carta): una rejilla con CSS Grid, 24px de relleno y 20px de separación entre platos; una columna en el móvil, dos columnas iguales a partir de 600px y tres a partir de 900px. Cada .plato con un borde de 1px, las esquinas redondeadas 8px y 16px de relleno.

index.htmlsolo lectura
estilos.css
Vista previa
CSSApartado 3 · CSSDifícil

Colores y tipografía: define en :root las variables --cafe: #6b3e26 y --crema: #fff8f0, usa --crema como fondo de la página y haz que el h1 crezca con la pantalla con clamp(1.75rem, 5vw, 3rem). El .boton es una píldora: inline-block, relleno de 12px arriba y abajo y 20px a los lados, esquinas de 999px, fondo --cafe, texto blanco y sin subrayado.

index.htmlsolo lectura
estilos.css
Vista previa
CSSApartado 4 · CSSDifícil

Los platos con la clase agotado se ven al 60 % de opacidad y llevan una etiqueta «Agotado» en la esquina superior derecha, hecha solo con CSS mediante ::after: posición absoluta a 8px del borde superior y del derecho (el plato tiene que ser su referencia), fondo #b00020 y texto blanco.

index.htmlsolo lectura
estilos.css
Vista previa
HTMLApartado 5 · HTMLDifícil

El formulario de reservas no es accesible: no tiene etiquetas, los tipos de campo no ayudan en el móvil y el botón es un div. Rehaz su HTML: cada campo con su <label for>; nombre y correo obligatorios con autocomplete (name y email) y el correo de tipo email; personas de tipo number entre 1 y 8; fecha de tipo date y obligatoria; el turno con dos radios (comida y cena, cada uno con su id y su label) dentro de un <fieldset> con la <legend> «Turno»; y un <button type="submit"> «Reservar». Conserva los id nombre, email, personas y fecha.

estilos.csssolo lectura
index.html
Vista previa

Solución explicada

Ver las soluciones de todos los apartados

Apartado 1 · CSS

css
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }

/* 1. Cabecera */
.cabecera {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 16px 24px;
}
.menu { display: flex; gap: 24px; }
@media (max-width: 599px) {
  .cabecera { flex-direction: column; align-items: flex-start; }
}

Apartado 2 · CSS

css
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }

/* 1. Cabecera */
.cabecera {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 16px 24px;
}
.menu { display: flex; gap: 24px; }
@media (max-width: 599px) {
  .cabecera { flex-direction: column; align-items: flex-start; }
}

/* 2. Carta */
.carta { display: grid; gap: 20px; padding: 24px; }
@media (min-width: 600px) { .carta { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .carta { grid-template-columns: repeat(3, 1fr); } }
.plato { border: 1px solid #d9cfc7; border-radius: 8px; padding: 16px; }

Apartado 3 · CSS

css
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }

/* 1. Cabecera */
.cabecera {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 16px 24px;
}
.menu { display: flex; gap: 24px; }
@media (max-width: 599px) {
  .cabecera { flex-direction: column; align-items: flex-start; }
}

/* 2. Carta */
.carta { display: grid; gap: 20px; padding: 24px; }
@media (min-width: 600px) { .carta { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .carta { grid-template-columns: repeat(3, 1fr); } }
.plato { border: 1px solid #d9cfc7; border-radius: 8px; padding: 16px; }

/* 3. Colores y tipografía */
:root {
  --cafe: #6b3e26;
  --crema: #fff8f0;
}
body { background: var(--crema); }
h1 { font-size: clamp(1.75rem, 5vw, 3rem); }
.boton {
  display: inline-block;
  padding: 12px 20px;
  border-radius: 999px;
  background: var(--cafe);
  color: #fff;
  text-decoration: none;
}

Apartado 4 · CSS

css
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }

/* 1. Cabecera */
.cabecera {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 16px 24px;
}
.menu { display: flex; gap: 24px; }
@media (max-width: 599px) {
  .cabecera { flex-direction: column; align-items: flex-start; }
}

/* 2. Carta */
.carta { display: grid; gap: 20px; padding: 24px; }
@media (min-width: 600px) { .carta { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .carta { grid-template-columns: repeat(3, 1fr); } }
.plato { border: 1px solid #d9cfc7; border-radius: 8px; padding: 16px; }

/* 3. Colores y tipografía */
:root {
  --cafe: #6b3e26;
  --crema: #fff8f0;
}
body { background: var(--crema); }
h1 { font-size: clamp(1.75rem, 5vw, 3rem); }
.boton {
  display: inline-block;
  padding: 12px 20px;
  border-radius: 999px;
  background: var(--cafe);
  color: #fff;
  text-decoration: none;
}

/* 4. Platos agotados */
.plato { position: relative; }
.plato.agotado { opacity: 0.6; }
.plato.agotado::after {
  content: "Agotado";
  position: absolute;
  top: 8px;
  right: 8px;
  padding: 2px 8px;
  border-radius: 4px;
  background: #b00020;
  color: #fff;
  font-size: 0.75rem;
}

Apartado 5 · HTML

markup
<section id="reservas">
  <h2>Reserva tu mesa</h2>
  <form class="reserva">
    <label for="nombre">Nombre</label>
    <input id="nombre" name="nombre" autocomplete="name" required>

    <label for="email">Correo</label>
    <input id="email" name="email" type="email" autocomplete="email" required>

    <label for="personas">Personas</label>
    <input id="personas" name="personas" type="number" min="1" max="8" value="2" required>

    <label for="fecha">Fecha</label>
    <input id="fecha" name="fecha" type="date" required>

    <fieldset>
      <legend>Turno</legend>
      <input type="radio" id="comida" name="turno" value="comida" required>
      <label for="comida">Comida</label>
      <input type="radio" id="cena" name="turno" value="cena">
      <label for="cena">Cena</label>
    </fieldset>

    <button type="submit">Reservar</button>
  </form>
</section>

La cabecera usa Flexbox con justify-content: space-between para separar logo y menú, y una media query que cambia la dirección a columna en el móvil. La carta usa Grid con repeat(n, 1fr): las columnas se reparten el ancho por igual y gap pone la separación sin márgenes que haya que compensar.

El enfoque mobile first deja el CSS base para el móvil y añade columnas con min-width. Las media queries se escriben de menor a mayor para que la más ancha gane cuando se cumplen varias.

Las variables CSS centralizan los colores de la marca y clamp() hace que el título crezca de forma continua entre un mínimo y un máximo. El pseudoelemento ::after añade la etiqueta sin tocar el HTML, y position: relative en el plato la ancla a su esquina.

El formulario accesible muestra que la accesibilidad empieza en el HTML: con label for, tipos de campo adecuados, fieldset y un button real, el formulario funciona con teclado, con lector de pantalla y con el teclado numérico o el calendario del móvil, sin una línea de JavaScript.

Para ir más allá

  • Añade un modo oscuro con @media (prefers-color-scheme: dark) redefiniendo solo las variables.
  • Convierte el menú en un botón desplegable en el móvil, con aria-expanded y JavaScript.
  • Pasa la página por Lighthouse y consigue 100 en accesibilidad.

Dónde se explica