Ya sabes seleccionar cualquier elemento con precisión y darle color, tamaño y tipografía. Lo que aún no controlas es cómo ocupa espacio cada caja ni cómo distribuir varias cajas en fila. Eso es precisamente lo que cubre este capítulo: primero el box model —cómo calcula el navegador el tamaño de cada elemento—, y después Flexbox, el modelo de layout para organizar elementos en una dimensión.
En este capítulo seguimos construyendo el Overwatch Team Builder. El HTML semántico ya está hecho y la tipografía está puesta; el reto ahora es maquetar la cabecera y las cartas con box model y Flexbox.
El box model: cómo calcula el navegador el tamaño de una caja#
Cada elemento HTML es una caja rectangular. Esa caja tiene cuatro capas, de dentro a fuera:
- Content — el área donde va el texto o los hijos.
- Padding — espacio interior entre el contenido y el borde. Tiene el color de fondo del elemento.
- Border — el borde visible (o invisible) que rodea el padding.
- Margin — espacio exterior que el elemento pide a sus vecinos. No tiene fondo.
La confusión habitual viene de width. Por defecto, width describe solo el área de
contenido. Si declaras width: 200px y luego añades padding: 20px, el elemento
acaba midiendo 240px: el navegador suma el padding por encima del ancho declarado.
Eso es box-sizing: content-box. El comportamiento que queremos es el contrario:
box-sizing: border-box;Con border-box, width incluye el padding y el borde. Si dices width: 200px, el
elemento mide exactamente 200px —y el área de contenido se reduce para acomodar el
padding—. Predecible y sensato. Es el primer reset que va en cualquier proyecto:
*,
*::before,
*::after {
box-sizing: border-box;
}Padding frente a margin: cuándo usar cada uno#
La regla práctica: usa padding para el espacio interior de una caja (la separación entre su borde y su contenido), y margin para separar esa caja de sus vecinos.
La diferencia se nota con el fondo: el color de fondo cubre el padding pero no el margin. En una carta de héroe, el padding es el espacio entre el borde de la tarjeta y el texto; el margin es el espacio entre una tarjeta y la siguiente.
Un aviso sobre el margin vertical: el colapso#
Hay un comportamiento del margin que sorprende a todo el mundo la primera vez, y es
mejor saberlo antes de chocar con él. Si un elemento tiene margin-bottom: 24px y el
de debajo margin-top: 16px, el hueco entre los dos no es 40px: es 24px, el mayor
de los dos. Los márgenes verticales entre elementos en bloque no se suman: se funden
en el más grande. A eso se le llama colapso de márgenes.
/* el hueco entre estos dos párrafos NO es 24 + 16 = 40px, sino 24px (el mayor) */
.parrafo-a {
/* margen inferior del primer párrafo */
margin-bottom: 24px;
}
.parrafo-b {
/* margen superior del segundo: no se suma al de arriba, se funde con él */
margin-top: 16px;
}Solo pasa con márgenes verticales entre elementos en bloque: no con el padding, ni
en horizontal, ni dentro de un contenedor flex (que verás justo abajo). Por ahora basta
con que lo reconozcas: cuando un hueco te salga más pequeño de lo que esperabas, el
colapso suele ser la causa. La forma más limpia de evitarlo es no separar con margin
sino con gap en el contenedor —lo ves en la sección siguiente—, que nunca colapsa.
Flexbox: distribuir elementos en una dimensión#
Flexbox es el modelo de layout para organizar elementos en una fila o en una columna. Activas Flexbox en el contenedor:
.site-header {
/* activa Flexbox: los hijos directos siguen las reglas del modelo flex */
display: flex;
}A partir de ahí, los hijos directos de .site-header se comportan según las reglas
de Flexbox. El contenedor tiene dos ejes:
- Eje principal — por defecto, horizontal (izquierda a derecha). Lo controlan
flex-directionyjustify-content. - Eje cruzado — perpendicular al eje principal, vertical por defecto. Lo controla
align-items.
Las propiedades que más vas a usar:
| Propiedad | Dónde va | Qué hace |
|---|---|---|
display: flex | contenedor | activa Flexbox |
flex-direction | contenedor | row (por defecto) o column |
justify-content | contenedor | distribución en el eje principal |
align-items | contenedor | alineación en el eje cruzado |
gap | contenedor | espacio entre hijos |
flex-grow | hijo | cuánto crece el hijo si sobra espacio |
flex-shrink | hijo | cuánto se encoge si falta espacio |
flex-basis | hijo | tamaño inicial antes de repartir el espacio |
justify-content: cómo repartes el espacio en la fila#
Con justify-content controlas qué pasa con el espacio sobrante en el eje principal:
flex-start— todo apilado al inicio (por defecto)flex-end— todo apilado al finalcenter— todo centradospace-between— primer elemento al inicio, último al final, sobrante entre ellosspace-around— el sobrante se reparte con márgenes iguales alrededor de cada elemento
Para la cabecera del Team Builder —brand a la izquierda, nav a la derecha— la opción
exacta es space-between.
align-items: alineación en el eje cruzado#
align-items alinea los hijos en la dirección perpendicular al eje principal. En una
fila horizontal, eso significa la alineación vertical:
stretch— los hijos estiran su altura para ocupar todo el contenedor (por defecto)flex-start— alineados al inicio (arriba en una fila horizontal)center— centrados verticalmentebaseline— alineados por su línea base tipográfica
Para alinear el brand y los enlaces al centro vertical, align-items: center.
gap: espaciado entre hijos#
gap es la forma correcta de separar los hijos de un contenedor flex. Define
el espacio entre ellos sin afectar los bordes del contenedor:
.hero-list {
/* activa Flexbox en la lista: los hijos directos se colocan en fila */
display: flex;
/* espacio entre cada carta de héroe, sin afectar los bordes del contenedor */
gap: 1rem;
}La alternativa —poner margin-right en cada hijo— obliga a anular el margen del
último elemento. gap no tiene ese problema: solo añade espacio entre hijos, nunca
fuera del primero ni del último.
flex-grow, flex-shrink y flex-basis#
Estas tres propiedades van en los hijos y le dicen a Flexbox cómo repartir el espacio sobrante o gestionar la falta de espacio:
flex-grow: 1— el elemento puede crecer para absorber el espacio libre. Si varios hijos tienenflex-grow: 1, lo reparten a partes iguales.flex-shrink: 0— el elemento no se encoge aunque falte espacio. Útil para el retrato circular: no queremos que se achate si el nombre del héroe es largo.flex-basis: 14rem— el tamaño inicial del elemento antes de repartir. Es más preciso quewidthen un contexto flex.
La abreviatura flex: 1 equivale a flex-grow: 1; flex-shrink: 1; flex-basis: 0, y
flex: none equivale a flex-grow: 0; flex-shrink: 0; flex-basis: auto.
flex-wrap: cuando los elementos no caben en una fila#
Por defecto, Flexbox intenta meter todos los hijos en una sola fila, encogiéndolos si
hace falta. Con flex-wrap: wrap, los hijos que no caben saltan a la siguiente fila:
.hero-list {
/* activa Flexbox en la lista */
display: flex;
/* permite que los hijos que no caben salten a la siguiente fila */
flex-wrap: wrap;
/* separación entre hijos */
gap: 1rem;
}
.hero-list li {
/* crece, se encoge, tamaño base 14rem */
flex: 1 1 14rem;
}Con esta configuración, con tres héroes salen tres columnas; si añades seis, se
organizan automáticamente en dos filas de tres. Sin flex-wrap, el sexto héroe se
encoge tanto que deja de ser legible.
Una nota sobre lo que se usaba antes#
Antes de que Flexbox estuviera disponible en todos los navegadores, los maquetadores
usaban float (pensado para que el texto fluyera alrededor de imágenes) e inline-block
para colocar elementos en fila. Seguirás viéndolos en código heredado. No merece la pena
aprenderlos como técnica de layout: Flexbox y Grid los reemplazan por completo, y si algún
día te los encuentras, las DevTools te darán contexto suficiente para entenderlos.
Pruébalo#
Esta es la cabecera y las cartas del Team Builder ya maquetadas. Abre la pestaña
/styles.css y modifica los valores que se mencionan en los comentarios del HTML:
cambia justify-content, activa o desactiva flex-wrap, observa qué hace flex: none
en el retrato. Los cambios son inmediatos en el preview.
Comprueba lo que sabes#
Pregunta 1 de 5
Declaras `width: 200px` y `padding: 20px` en un elemento. ¿Cuánto mide en total si NO has puesto `box-sizing: border-box`?
Tu turno#
El HTML ya está hecho. Tu tarea es completar los TODO del fichero CSS, aquí mismo. Escribe las reglas de box model y Flexbox para que la cabecera y las cartas queden bien maquetadas. Cuando creas que lo tienes, despliega las soluciones y compáralas con la tuya.
Ejercicio · en esta página
Maqueta la cabecera y las cartas del Team Builder
El HTML de la página ya está hecho: una cabecera con su navegación y una lista de tres cartas de héroe. Tu tarea es escribir el CSS para maquetar esa estructura con box model y Flexbox. Completa los TODO de starter/styles.css hasta que la página se vea limpia y bien espaciada.
Paso 1: Que funcione
- La cabecera muestra brand y nav en fila.
- Las tres cartas se ven en fila, con retrato, nombre, rol y estadísticas.
- Hay padding y bordes en las cartas, y separación entre las estadísticas.
Paso 2: Flexbox de verdad
- `box-sizing: border-box` aplicado globalmente.
- El espaciado entre elementos usa `gap`, no `margin` suelto.
- Tamaños en `rem`, no en `px`.
- `flex: none` en el retrato para que no se encoja.
- Sin anchos fijos en las cartas.
Paso 3: Robusto y mantenible
- `flex-wrap` en la lista: si hay más héroes, saltan a una segunda fila.
- `min-width: 0` en los `li` para evitar el bug de desbordamiento de texto.
- Sin alturas fijas: las cajas crecen con su contenido.
- Los valores repetidos (color de acento, radios) están anotados con un comentario indicando que piden una sola fuente de verdad —las variables CSS del capítulo siguiente—.
Ver soluciones
/* =============================================================================
NIVEL OK — "funciona y se ve"
=============================================================================
Resuelve todos los TODO del starter. La página se ve correctamente.
Sus límites (lo que arregla el nivel "Mejor"):
- Números mágicos por todas partes: 48px, 20px, 220px... Si el diseño
cambia, hay que cazar esos valores uno a uno por todo el fichero.
- Espaciado a base de margin sueltos en lugar de gap. El resultado es el
mismo, pero gap es la herramienta correcta en un contenedor flex/grid.
- El retrato y la carta tienen ancho y alto fijos en píxeles: funcionan
para estos tres héroes, pero con un nombre largo como "Brigitte Lindholm"
la tarjeta se rompe.
- box-sizing no está declarado globalmente: el padding suma al tamaño, así
que los cálculos de ancho son frágiles.
Aun así: se ve y ocupa el espacio correcto. Es un punto de partida válido.
============================================================================= */
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.5;
}
.page {
max-width: 960px;
margin: 0 auto;
padding: 40px 24px 64px;
}
/* --- Cabecera --- */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e6e3ee;
padding-bottom: 20px;
margin-bottom: 32px;
}
.brand {
font-size: 1.4rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0;
}
.brand span {
color: #b8336a;
}
.site-nav a {
color: #5b5966;
text-decoration: none;
font-size: 0.9rem;
margin-left: 16px;
}
.site-nav a:hover {
color: #b8336a;
}
/* --- Lista de héroes --- */
.section-title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #5b5966;
margin: 0 0 16px;
}
.hero-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
.hero-list li {
margin-right: 16px;
width: 220px;
}
/* --- Carta de héroe --- */
.hero-card {
background: #fff;
border: 1px solid #e6e3ee;
border-radius: 12px;
padding: 20px;
}
.hero-top {
display: flex;
align-items: center;
}
.hero-portrait {
width: 48px;
height: 48px;
border-radius: 50%;
background: #b8336a;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-right: 14px;
}
.hero-name {
font-size: 1.05rem;
font-weight: 700;
margin: 0;
}
.hero-role {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #5b5966;
margin: 0.1rem 0 0;
}
.hero-stats {
display: flex;
margin: 12px 0 0;
padding-top: 8px;
border-top: 1px solid #e6e3ee;
}
.stat {
display: flex;
flex-direction: column;
margin-right: 20px;
}
.stat dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #5b5966;
margin: 0;
}
.stat dd {
font-size: 1.05rem;
font-weight: 600;
margin: 0.15rem 0 0;
}
/* --- Pie --- */
.site-footer {
margin-top: 3rem;
padding-top: 1.25rem;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 0.85rem;
} Por qué este nivel
- Resuelve todos los TODO y la página se ve correctamente. Ese es el objetivo mínimo.
- Usa `margin` suelto para separar elementos en lugar de `gap`, y hay números mágicos en px repartidos por el fichero (48px, 220px, 20px...).
- Sin `box-sizing: border-box` global: el padding suma al `width` declarado, así que el tamaño real de las cajas es difícil de predecir.
- Con anchos fijos en las cartas y el retrato sin `flex: none`, un nombre largo como "Brigitte Lindholm" descolocaría el layout.
/* =============================================================================
NIVEL MEJOR — "Flexbox de verdad"
=============================================================================
Mismo aspecto que el nivel OK. Lo que cambia es la solidez del código.
Por qué mejora al nivel OK:
- box-sizing: border-box global: el padding ya no suma al ancho declarado.
Es el primer reset que se pone en cualquier proyecto profesional.
- gap en lugar de margin sueltos para el espaciado entre elementos flex.
Gap es la herramienta nativa del modelo flex/grid; margin es un parche.
- align-items en .hero-top: la alineación es explícita y semántica, no un
efecto secundario de que los elementos "casualmente" se vean bien.
- rem en lugar de px para los tamaños: se adapta a la preferencia de fuente
del usuario (alguien que tiene el navegador a 20px base lo agradece).
- El retrato usa flex: none para que no se encoja si el nombre del héroe es
largo. Sin esto, en un nombre como "Brigitte" la tarjeta cojea.
- No hay anchos fijos en las cartas: crecen o se encogen con el espacio
disponible gracias a flex por defecto.
Lo que aún pule el nivel Excelente: que la lista de héroes aguante cuando hay
más de tres, que las cartas tengan un mínimo razonable, y variables CSS para
que cambiar el espaciado sea una sola edición.
============================================================================= */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.5;
}
.page {
max-width: 60rem;
margin: 0 auto;
padding: 2.5rem 1.5rem 4rem;
}
/* --- Cabecera --- */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
border-bottom: 1px solid #e6e3ee;
padding-bottom: 1.25rem;
margin-bottom: 2rem;
}
.brand {
font-size: 1.4rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0;
}
.brand span {
color: #b8336a;
}
.site-nav {
display: flex;
gap: 1rem;
}
.site-nav a {
color: #5b5966;
text-decoration: none;
font-size: 0.9rem;
}
.site-nav a:hover {
color: #b8336a;
}
/* --- Lista de héroes --- */
.section-title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #5b5966;
margin: 0 0 1rem;
}
.hero-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 1rem;
}
/* --- Carta de héroe --- */
.hero-card {
background: #fff;
border: 1px solid #e6e3ee;
border-radius: 12px;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.hero-top {
display: flex;
align-items: center;
gap: 0.85rem;
}
.hero-portrait {
flex: none;
width: 3rem;
height: 3rem;
border-radius: 50%;
background: #b8336a;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.hero-name {
font-size: 1.05rem;
font-weight: 700;
margin: 0;
}
.hero-role {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #5b5966;
margin: 0.1rem 0 0;
}
.hero-stats {
display: flex;
gap: 1.25rem;
margin: 0;
padding-top: 0.5rem;
border-top: 1px solid #e6e3ee;
}
.stat {
display: flex;
flex-direction: column;
}
.stat dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #5b5966;
margin: 0;
}
.stat dd {
font-size: 1.05rem;
font-weight: 600;
margin: 0.15rem 0 0;
}
/* --- Pie --- */
.site-footer {
margin-top: 3rem;
padding-top: 1.25rem;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 0.85rem;
} Por qué es mejor que el anterior
- `box-sizing: border-box` en el reset global: a partir de aquí, `width` incluye padding y borde. Es el primer reset que se pone en cualquier proyecto real.
- `gap` reemplaza a todos los `margin` de separación entre ítems flex. La diferencia con `margin`: `gap` solo afecta el espacio entre hijos del contenedor, no añade espacio exterior.
- `flex: none` en el retrato: sin esta declaración, Flexbox puede encogerlo si el nombre del héroe es largo. `none` equivale a `flex-grow: 0; flex-shrink: 0`.
- Sin anchos fijos: las cartas usan el espacio disponible. El único cambio visible respecto al nivel OK es que el código es mucho más fácil de mantener.
/* =============================================================================
NIVEL EXCELENTE — "robusto y mantenible"
=============================================================================
Misma pantalla que los niveles anteriores. Lo que cambia es que este CSS no
se pelea con el navegador: aguanta contenido real sin romperse.
Por qué mejora al nivel Mejor (todo con lo que ya sabes: box model + Flexbox):
- flex-wrap en .hero-list: si en el futuro hay seis héroes en lugar de tres,
la lista salta a una segunda fila. Sin wrap, los elementos se encogen hasta
hacerse ilegibles o se salen del contenedor.
- min-width: 0 en los .hero-list li: resuelve un bug sutil de Flexbox donde
los elementos con texto largo ignoran el límite del contenedor. Es una de
esas reglas que no hacen falta hasta que las necesitas, y entonces las
necesitas urgente.
- Sin alturas fijas en ninguna parte: las cajas crecen con su contenido. Un
héroe con una descripción larga no desborda ni trunca.
- flex: none en .hero-portrait: ese círculo nunca debe encogerse aunque el
nombre del héroe sea largo.
Una pega que se nota: el acento #b8336a y los espaciados se repiten a mano por
toda la hoja. Cambiar el acento serían doce ediciones. Eso pide una sola fuente
de verdad —las variables CSS— y un layout de página con CSS Grid. Las dos cosas
son justo el siguiente capítulo: aquí el salto está señalado, no dado.
============================================================================= */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.5;
}
.page {
max-width: 60rem;
margin: 0 auto;
padding: 2.5rem 1.5rem 4rem;
}
/* --- Cabecera --- */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
border-bottom: 1px solid #e6e3ee;
padding-bottom: 1.25rem;
margin-bottom: 2rem;
}
.brand {
font-size: 1.4rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0;
}
.brand span {
color: #b8336a;
}
.site-nav {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.site-nav a {
color: #5b5966;
text-decoration: none;
font-size: 0.9rem;
}
.site-nav a:hover {
color: #b8336a;
}
/* --- Lista de héroes --- */
.section-title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #5b5966;
margin: 0 0 1rem;
}
.hero-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.hero-list li {
flex: 1 1 15rem;
min-width: 0;
}
/* --- Carta de héroe --- */
.hero-card {
background: #ffffff;
border: 1px solid #e6e3ee;
border-radius: 12px;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.hero-top {
display: flex;
align-items: center;
gap: 0.85rem;
}
.hero-portrait {
flex: none;
width: 3rem;
height: 3rem;
border-radius: 50%;
background: #b8336a;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.hero-info {
min-width: 0;
}
.hero-name {
font-size: 1.05rem;
font-weight: 700;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hero-role {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #5b5966;
margin: 0.1rem 0 0;
}
.hero-stats {
display: flex;
gap: 1.25rem;
margin: 0;
padding-top: 0.5rem;
border-top: 1px solid #e6e3ee;
}
.stat {
display: flex;
flex-direction: column;
}
.stat dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #5b5966;
margin: 0;
}
.stat dd {
font-size: 1.05rem;
font-weight: 600;
margin: 0.15rem 0 0;
}
/* --- Pie --- */
.site-footer {
margin-top: 3rem;
padding-top: 1.25rem;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 0.85rem;
} Por qué es mejor que el anterior
- `flex-wrap` en la lista de héroes: con tres héroes no se nota, pero si mañana hay seis, la lista organiza dos filas de forma automática. Sin `flex-wrap`, los elementos se encogen o se salen del contenedor.
- `min-width: 0` en los `li` resuelve uno de los bugs más habituales de Flexbox: por defecto, un ítem flex no puede ser más estrecho que su contenido mínimo. Con un nombre de héroe largo, el ítem se sale del ancho esperado. `min-width: 0` lo deja encogerse.
- Sin alturas fijas en ninguna parte: las cajas crecen con su contenido. Este es el comportamiento natural del navegador; el nivel OK lo forzaba con píxeles. Aquí nos limitamos a no pelearnos con él.
- Los colores y el radio del borde están escritos a mano en varios sitios. El fichero lo anota: cuando un valor se repite así, pide una sola fuente de verdad. Esa herramienta son las variables CSS —las verás en el capítulo 10.