Maquetaciones típicas en CSS: holy grail, pie pegado abajo, hero y más
En esta lección
La mayoría de las webs se construyen con un puñado de esquemas que se repiten: una cabecera que se queda arriba, un pie que no flota a media pantalla, una barra lateral, una portada que ocupa toda la pantalla, filas de tarjetas de la misma altura… En esta lección tienes un recetario: cada maquetación con su problema, su solución con Grid o Flexbox (o con las dos) y los detalles que hacen que funcione en el móvil.
Necesitas tener claros Flexbox, Grid y Grid a fondo. Para centrar cosas sueltas tienes su propia lección: centrar elementos.
El contenedor centrado
Casi todas las páginas limitan el ancho del contenido y lo centran, para que en una pantalla grande las líneas no sean eternas. Una sola regla:
.contenedor {
width: min(100% - 2rem, 70rem); /* 70rem como mucho, y 1rem de aire a cada lado en el móvil */
margin-inline: auto;
}
min() elige el menor de los dos valores: en un móvil, el ancho de la pantalla menos 2rem; en un portátil, 70rem. Lo usarás dentro de casi todas las recetas siguientes.
Pie pegado abajo
El problema: en una página con poco contenido (un “Gracias por tu compra”, un 404), el pie aparece a mitad de pantalla y por debajo queda un hueco vacío.
La idea: la página debe medir al menos el alto de la ventana, y el contenido principal se queda con todo el espacio sobrante.
<body>
<header>Cabecera</header>
<main>Contenido</main>
<footer>Pie</footer>
</body>
Con Grid
body {
min-height: 100dvh;
margin: 0;
display: grid;
grid-template-rows: auto 1fr auto; /* cabecera, contenido (lo que sobre), pie */
}
La fila del medio es 1fr: se queda con todo el alto libre y empuja el pie hasta abajo. Si el contenido es largo, la página crece con normalidad y el pie va detrás.
Con Flexbox
body {
min-height: 100dvh;
margin: 0;
display: flex;
flex-direction: column;
}
main { flex-grow: 1; } /* el contenido se estira; también vale footer { margin-top: auto; } */
Las dos funcionan igual de bien. La versión de Grid tiene una pega: cuenta todos los hijos de <body>. Si hay un cuarto elemento (un aviso de cookies, un <script> no cuenta), tendrás que añadir una fila. La de Flexbox no depende del número de hijos.
Consejo: usa
min-height, nuncaheight. Conheight: 100dvh, si el contenido es más largo que la pantalla, se sale de la caja por debajo. Y prefieredvhavhpor la barra del navegador del móvil, como viste en unidades.
Cabecera fija
El problema: quieres que el menú siga visible mientras bajas por la página.
position: sticky (la opción habitual)
.cabecera {
position: sticky;
top: 0; /* se pega al llegar al borde superior */
z-index: 10; /* por encima del contenido que pasa por debajo */
background: white; /* sin fondo, verías el texto pasar a través */
}
Un elemento sticky ocupa su sitio normal en la página y, al hacer scroll, se queda pegado cuando llega a top: 0. Como no sale del flujo, no tapa el principio del contenido.
Funciona solo mientras su padre está a la vista. Por eso la cabecera debe ser hija directa de <body> (o de un contenedor tan alto como la página). Si la metes en un <div> que mide lo mismo que ella, se despega enseguida.
position: fixed (cuando debe estar siempre ahí)
.cabecera {
position: fixed;
inset-block-start: 0;
inset-inline: 0; /* de lado a lado */
height: 64px;
}
body { padding-top: 64px; } /* hueco para que no tape el principio */
fixed saca la cabecera del flujo: el resto de la página sube y queda tapado. Por eso hay que reservar el hueco a mano, sabiendo su altura. Úsalo solo si sticky no te sirve.
Los enlaces internos quedan tapados
Con cualquiera de las dos, al pulsar un enlace como #horarios la sección queda debajo de la cabecera. Se arregla en una línea:
html { scroll-padding-top: 80px; } /* el alto de la cabecera y un poco de aire */
Más detalles en scroll. Y si dentro de la cabecera colocas el logo a un lado y los enlaces al otro, es un trabajo para Flexbox: display: flex; justify-content: space-between; align-items: center;.
Barra lateral
El problema: una zona estrecha (filtros, índice, menú) junto al contenido, que en el móvil debe ir arriba o abajo.
Con Grid y una media query
.con-lateral {
display: grid;
gap: 24px;
}
@media (width >= 768px) {
.con-lateral { grid-template-columns: 250px minmax(0, 1fr); }
}
En móvil hay una sola columna; desde 768px, la barra mide 250px y el contenido el resto. minmax(0, 1fr) en lugar de 1fr evita que una tabla o una URL larga ensanchen la columna y rompan el diseño.
Con Flexbox y sin media queries
Esta receta decide sola cuándo poner la barra al lado y cuándo debajo, según el espacio:
.con-lateral {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.lateral {
flex-basis: 250px; /* ancho ideal de la barra */
flex-grow: 1;
}
.principal {
flex-basis: 0;
flex-grow: 999; /* se queda con casi todo el espacio libre */
min-inline-size: 60%; /* si no puede medir al menos el 60 %, baja a otra línea */
}
Mientras quepan, el contenido principal se lleva casi todo el espacio (999 partes contra 1) y la barra se queda con sus 250px. Cuando el contenido ya no puede medir el 60 % del ancho, salta a la línea siguiente y los dos se estiran al 100 %. Funciona igual dentro de un contenedor estrecho, sin depender del tamaño de la pantalla.
Barra lateral que acompaña el scroll
Un índice que se queda a la vista mientras lees un artículo largo:
.lateral {
position: sticky;
top: 80px; /* por debajo de la cabecera fija */
align-self: start; /* imprescindible en Grid y en Flexbox */
}
Sin align-self: start, la barra se estira hasta la altura del contenido (es el comportamiento por defecto, stretch) y ya no tiene espacio para “pegarse”: parece que sticky no funciona.
Holy grail
El holy grail (santo grial) es el esquema clásico de cinco zonas: cabecera, pie y, en medio, tres columnas: menú, contenido y una barra extra. Durante años fue muy difícil hacerlo bien con CSS, de ahí el nombre. Con Grid es un dibujo:
<body>
<header>Cabecera</header>
<nav>Menú</nav>
<main>Contenido</main>
<aside>Barra extra</aside>
<footer>Pie</footer>
</body>
body {
min-height: 100dvh;
margin: 0;
display: grid;
grid-template-rows: auto auto 1fr auto auto;
grid-template-areas: "cabecera" "menu" "contenido" "extra" "pie";
}
@media (width >= 900px) {
body {
grid-template-columns: 200px minmax(0, 1fr) 200px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"cabecera cabecera cabecera"
"menu contenido extra"
"pie pie pie";
}
}
header { grid-area: cabecera; }
nav { grid-area: menu; }
main { grid-area: contenido; }
aside { grid-area: extra; }
footer { grid-area: pie; }
Fíjate en que de regalo tienes el pie pegado abajo: la fila del contenido es 1fr y el <body> mide al menos la pantalla.
Con Flexbox
Con Flexbox necesitas un contenedor más, porque Flexbox trabaja en una sola dirección: una columna para la página y una fila para el centro.
<body>
<header>Cabecera</header>
<div class="centro">
<nav>Menú</nav>
<main>Contenido</main>
<aside>Barra extra</aside>
</div>
<footer>Pie</footer>
</body>
body { min-height: 100dvh; margin: 0; display: flex; flex-direction: column; }
.centro { flex-grow: 1; display: flex; flex-wrap: wrap; }
.centro nav, .centro aside { flex: 1 1 200px; }
.centro main { flex: 3 1 400px; }
Funciona, pero los dos laterales no se recolocan con la libertad de Grid: cuando no caben, simplemente bajan de línea. Para la estructura de la página, Grid suele ser la mejor herramienta.
Hero a pantalla completa
El hero es la portada grande con la que abren muchas webs: una imagen que ocupa toda la pantalla y un titular encima.
<section class="hero">
<img src="img/costa.jpg" alt="Acantilados al atardecer">
<div class="hero-texto">
<h1>Descubre la costa</h1>
<a class="boton" href="/rutas">Ver rutas</a>
</div>
</section>
.hero {
min-height: 100svh;
display: grid;
}
.hero > * { grid-area: 1 / 1; } /* imagen y texto en la misma celda */
.hero img {
width: 100%;
height: 100%;
object-fit: cover; /* llena la pantalla sin deformarse */
}
.hero-texto {
place-self: center; /* el texto, en el centro */
padding: 24px;
text-align: center;
color: white;
text-shadow: 0 2px 8px rgb(0 0 0 / 0.6);
}
Es la técnica de la pila de Grid a fondo: imagen y texto comparten celda y no hace falta position: absolute. Si el texto no se lee bien sobre la foto, añade un degradado oscuro en una capa más (lo tienes en degradados).
¿Por qué svh y no dvh? Los dos tienen en cuenta la barra de direcciones del móvil, pero dvh cambia de valor cuando esa barra se esconde al hacer scroll, y el hero “salta”. svh (small viewport height) usa siempre el alto con la barra visible y no cambia. Para el pie pegado abajo da igual; para algo tan grande como un hero, svh es más estable.
Cuidado: no pongas
height: 100svhfijo. En un móvil apaisado, el texto del hero puede no caber y se cortaría. Conmin-height, la sección crece si hace falta.
Tarjetas de la misma altura
El problema: una fila de tarjetas con textos de distinta longitud. Quieres que todas midan lo mismo y que el botón quede siempre abajo.
Con Grid (las alturas iguales salen solas)
.tarjetas {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr));
gap: 20px;
}
.tarjeta {
display: flex;
flex-direction: column; /* el contenido, en columna */
}
.tarjeta .boton {
margin-top: auto; /* empuja el botón al fondo */
}
Dos ideas trabajan juntas:
- En Grid, todas las celdas de una fila miden lo mismo que la más alta, y las tarjetas se estiran (
align-items: stretchpor defecto). Ya tienen la misma altura. - Dentro de cada tarjeta, Flexbox en columna con
margin-top: autoen el botón hace que todo el espacio libre quede encima de él: el botón baja al fondo.
Si además quieres que los títulos y los párrafos empiecen a la misma altura en todas, usa subgrid.
Con Flexbox
.tarjetas { display: flex; flex-wrap: wrap; gap: 20px; }
.tarjeta { flex: 1 1 250px; display: flex; flex-direction: column; }
.tarjeta .boton { margin-top: auto; }
Las alturas también se igualan dentro de cada línea, pero hay una diferencia: si en la última línea quedan dos tarjetas, se estiran hasta ocupar todo el ancho y salen más anchas que las de arriba. Con Grid todas miden lo mismo. Por eso, para rejillas de tarjetas, Grid es mejor opción.
Columnas de texto
Para repartir un texto en varias columnas, como en un periódico, ni Grid ni Flexbox sirven: no saben partir un párrafo entre dos columnas. Para eso está multicolumna:
.noticia {
columns: 3 16rem; /* hasta 3 columnas de al menos 16rem */
column-gap: 2rem;
column-rule: 1px solid #cbd5e1; /* línea separadora entre columnas */
}
.noticia h2 { column-span: all; } /* el titular atraviesa todas las columnas */
.noticia figure { break-inside: avoid; } /* no partas una imagen entre columnas */
columns junta el número máximo de columnas y su ancho mínimo. En un móvil saldrá una sola, sin media queries. El texto llena la primera columna, sigue por la segunda, y así.
Úsalas con cabeza. En una pantalla, un texto largo en columnas obliga a bajar hasta el final de la primera y volver a subir para leer la segunda. Funcionan bien en bloques cortos que se ven enteros: una lista de enlaces del pie, un índice, un glosario o una lista de ingredientes.
.lista-tiendas {
columns: 12rem; /* tantas columnas de 12rem como quepan */
}
.lista-tiendas li { break-inside: avoid; }
Un truco: con columns y break-inside: avoid en cada tarjeta se consigue un mosaico tipo Pinterest, con tarjetas de distinta altura encajadas. La pega es que el orden va de arriba abajo por columnas, no por filas.
¿Qué receta uso?
| Necesito… | Receta | Herramienta |
|---|---|---|
| Limitar el ancho y centrar | width: min(...) + margin-inline: auto | Ninguna especial |
| Que el pie no flote | Fila 1fr o flex-grow: 1 + min-height: 100dvh | Grid o Flexbox |
| Menú siempre visible | position: sticky; top: 0 | Posicionamiento |
| Zona lateral | Columnas fijas o el truco de flex-grow: 999 | Grid o Flexbox |
| Cinco zonas clásicas | Holy grail con áreas | Grid |
| Portada a pantalla completa | Pila con grid-area: 1 / 1 + min-height: 100svh | Grid |
| Tarjetas iguales | auto-fill + Flexbox en columna dentro | Grid + Flexbox |
| Texto en columnas | columns | Multicolumna |
Errores frecuentes
- Usar
height: 100vhen lugar demin-height: 100dvh. El contenido largo se sale de la caja y en el móvil la barra del navegador tapa una parte. - Meter la cabecera sticky en un contenedor de su misma altura. El elemento sticky solo se pega dentro de su padre; si el padre es igual de alto, no tiene recorrido.
- Olvidar el fondo de la cabecera fija. El contenido se ve pasar a través del texto.
- Barra lateral sticky que no se pega. Le falta
align-self: start: estirada a la altura del contenido, no tiene dónde moverse. - Usar
1frpara el contenido junto a una barra lateral y que una tabla ancha lo rompa. Usaminmax(0, 1fr). - Poner
fixedy olvidar el hueco. El principio de la página queda debajo de la cabecera. Prefieresticky. - Texto largo en columnas. Obliga a subir y bajar para leer. Resérvalas para bloques cortos.
- Igualar alturas con
heightfijo. Si un texto crece, se sale de la tarjeta. Deja que Grid estire las celdas.
Resumen
| Maquetación | Código clave |
|---|---|
| Contenedor centrado | width: min(100% - 2rem, 70rem); margin-inline: auto; |
| Pie pegado abajo | min-height: 100dvh; display: grid; grid-template-rows: auto 1fr auto; |
| Cabecera fija | position: sticky; top: 0; z-index: 10; |
| Enlaces bajo la cabecera | html { scroll-padding-top: 80px; } |
| Barra lateral | grid-template-columns: 250px minmax(0, 1fr); |
| Lateral sticky | position: sticky; top: 80px; align-self: start; |
| Holy grail | grid-template-areas con tres columnas y pie pegado |
| Hero | min-height: 100svh; + pila con grid-area: 1 / 1 |
| Tarjetas iguales | Grid con auto-fill + .boton { margin-top: auto; } |
| Columnas de texto | columns: 3 16rem; column-span: all; break-inside: avoid; |
Ya sabes construir el esqueleto de cualquier página. Ahora vamos con lo que va dentro: sigue con imágenes en CSS.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.