Ejercicios de CSS resueltos
En esta lección
La mejor forma de aprender CSS es escribirlo. En esta lección tienes 12 ejercicios ordenados de más fácil a más difícil que repasan todo el curso: selectores, especificidad, modelo de caja, Flexbox, Grid, diseño responsive, variables y transiciones. Cada uno trae el HTML de partida y la solución oculta. Intenta resolverlo tú primero, en el editor “Pruébalo” o en tu ordenador, y abre la solución solo para comparar.
Cómo trabajar los ejercicios
- Copia el HTML en un archivo
index.htmlcon su<link rel="stylesheet" href="estilos.css">, o pégalo en el editor de esta página. - Escribe tu CSS antes de mirar la solución. Si te atascas más de 15 minutos, vuelve a la lección enlazada en el enunciado.
- Usa las DevTools (F12): inspecciona el elemento, mira qué reglas se aplican y cuáles aparecen tachadas.
- Tu solución no tiene por qué ser idéntica a la propuesta. En CSS casi siempre hay varias formas correctas; lo importante es que el resultado sea el pedido y que entiendas por qué funciona.
En todos los ejercicios se supone esta base, que ya conoces de lecciones anteriores:
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.5; }
Nivel 1: selectores y cascada
Ejercicio 1: la carta de una cafetería
Tienes la carta de una cafetería. Sin añadir clases nuevas al HTML, consigue que:
- Los títulos
h2de cada sección sean de color#7b3f00. - Los precios (
spancon claseprecio) vayan en negrita y de color#2e7d32. - El plato marcado como recomendado (
licon claserecomendado) tenga fondo#fff4d6. - Solo los párrafos que están dentro de
.cartavayan en cursiva; el párrafo del pie no.
<main class="carta">
<h2>Cafés</h2>
<p>Todos con leche de la granja de al lado.</p>
<ul>
<li>Café solo <span class="precio">1,30 €</span></li>
<li class="recomendado">Capuchino <span class="precio">2,10 €</span></li>
</ul>
<h2>Dulces</h2>
<ul>
<li>Sobao pasiego <span class="precio">1,80 €</span></li>
</ul>
</main>
<footer><p>Abrimos de 8:00 a 20:00.</p></footer>
Ver solución
h2 {
color: #7b3f00;
}
.precio {
font-weight: bold;
color: #2e7d32;
}
.recomendado {
background: #fff4d6;
}
.carta p {
font-style: italic;
}
.carta p es un selector descendiente: selecciona los p que están en cualquier nivel dentro de .carta. El párrafo del pie está fuera, así que no le afecta. Repasa los tipos de selectores en Selectores.
Ejercicio 2: una tabla de clasificación
Una tabla con la clasificación de una liga de baloncesto de barrio. Sin tocar el HTML:
- Las filas pares del cuerpo (
tbody) con fondo#f1f4f9. - Al pasar el ratón por una fila, fondo
#dce7f7. - La primera fila del cuerpo (el líder) en negrita.
- Los enlaces que empiezan por
https://(webs externas) con una flecha↗detrás, generada con CSS.
<table class="clasificacion">
<thead>
<tr><th>Equipo</th><th>Puntos</th></tr>
</thead>
<tbody>
<tr><td><a href="https://ejemplo.com/canteras">Canteras BC</a></td><td>18</td></tr>
<tr><td><a href="/equipos/puerto">Puerto Azul</a></td><td>15</td></tr>
<tr><td><a href="/equipos/rias">Rías Altas</a></td><td>12</td></tr>
<tr><td><a href="https://ejemplo.com/sardinero">Sardinero</a></td><td>9</td></tr>
</tbody>
</table>
Ver solución
.clasificacion tbody tr:nth-child(even) {
background: #f1f4f9;
}
.clasificacion tbody tr:hover {
background: #dce7f7;
}
.clasificacion tbody tr:first-child {
font-weight: bold;
}
a[href^="https://"]::after {
content: " ↗";
}
:nth-child(even) selecciona los hijos en posición par. El selector de atributo [href^="..."] significa “empieza por”. Y ::after es un pseudoelemento que inserta contenido decorativo después del contenido del elemento. Fíjate en que la regla de :hover va después de la de las filas pares: tienen la misma especificidad, así que gana la última. Tienes más en Pseudoclases a fondo.
Ejercicio 3: ¿de qué color sale?
Este ejercicio es de pensar, no de escribir. Sin probarlo, decide de qué color se ve el texto de cada uno de los tres párrafos. Después compruébalo.
<section id="avisos" class="panel">
<p class="aviso">Primero</p>
<p class="aviso importante">Segundo</p>
<p style="color: purple" class="aviso">Tercero</p>
</section>
#avisos p { color: blue; }
.panel .aviso { color: green; }
.aviso.importante { color: red; }
p { color: orange !important; }
Ver solución
Los tres párrafos salen naranjas.
La declaración p { color: orange !important; } tiene !important, y una declaración importante gana a todas las normales, sea cual sea su especificidad e incluso al atributo style. Por eso es tan peligroso usarlo.
Si quitas el !important, el resultado cambia:
| Párrafo | Gana | Por qué |
|---|---|---|
| Primero | azul | #avisos p tiene un id: especificidad (1,0,1), la mayor |
| Segundo | azul | .aviso.importante es (0,2,0), pero un id siempre gana a cualquier número de clases |
| Tercero | morado | El atributo style gana a cualquier selector de la hoja |
Repasa cómo se calcula la especificidad en Cascada y herencia.
Nivel 2: modelo de caja
Ejercicio 4: calcula el ancho
Dos cajas con las mismas propiedades, pero distinto box-sizing. ¿Cuánto mide cada una de ancho total en pantalla, desde el borde exterior izquierdo hasta el derecho (sin contar el margen)? ¿Y cuánto ocupa contando el margen?
.caja {
width: 300px;
padding: 20px;
border: 5px solid black;
margin: 10px;
}
.caja--a { box-sizing: content-box; }
.caja--b { box-sizing: border-box; }
Ver solución
| Caja | Ancho visible (borde a borde) | Espacio ocupado con margen |
|---|---|---|
.caja--a (content-box) | 300 + 20 + 20 + 5 + 5 = 350px | 350 + 10 + 10 = 370px |
.caja--b (border-box) | 300px (el contenido se encoge a 250px) | 300 + 10 + 10 = 320px |
Con content-box, el width es solo el contenido y el relleno y el borde se suman por fuera. Con border-box, el width incluye relleno y borde. El margen nunca forma parte del width. Puedes verlo en el diagrama de caja de la pestaña “Calculado” de DevTools. Más en Modelo de caja.
Ejercicio 5: tarjeta de un socio
Maqueta la tarjeta de socio de una biblioteca:
- Ancho máximo de 360px, centrada horizontalmente en la página.
- Relleno de 24px, borde de 1px gris claro (
#d0d5dd), esquinas redondeadas de 12px y una sombra suave. - La foto, redonda, de 80px de lado.
- El número de socio con letra monoespaciada.
- Separación de 16px entre la tarjeta y lo que tenga encima y debajo.
<article class="socio">
<img class="socio__foto" src="https://picsum.photos/id/64/160/160" alt="Foto de Inés Gómez">
<h2 class="socio__nombre">Inés Gómez</h2>
<p class="socio__numero">Socia n.º 004812</p>
</article>
Ver solución
.socio {
max-width: 360px;
margin: 16px auto;
padding: 24px;
border: 1px solid #d0d5dd;
border-radius: 12px;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.08);
text-align: center;
}
.socio__foto {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.socio__nombre {
margin: 12px 0 4px;
}
.socio__numero {
margin: 0;
font-family: ui-monospace, monospace;
color: #555;
}
margin: 16px auto da 16px arriba y abajo y reparte el espacio sobrante a izquierda y derecha, lo que centra una caja de bloque que tiene un ancho limitado. Usamos max-width y no width para que en un móvil de 320px la tarjeta se estreche en lugar de desbordar. object-fit: cover evita que la foto se deforme si no es cuadrada. Otras formas de centrar en Centrar elementos.
Nivel 3: Flexbox
Ejercicio 6: barra de navegación
Usa el HTML del editor “Pruébalo” de esta página (la cabecera de una librería). Consigue que:
- El logo quede a la izquierda y el menú a la derecha, ambos centrados verticalmente.
- Los enlaces del menú estén en fila con 20px de separación entre ellos.
- En pantallas de menos de 500px, el logo y el menú se apilen (logo arriba, menú debajo) con 8px de separación.
Ver solución
.barra {
display: flex;
flex-direction: column;
gap: 8px;
}
.barra__menu {
display: flex;
gap: 20px;
}
@media (width >= 500px) {
.barra {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
Lo escribimos mobile first: la versión base es la apilada y la media query añade la fila a partir de 500px. Hay dos contenedores flex: .barra coloca logo y nav, y .barra__menu coloca los li. justify-content: space-between manda el primer hijo al principio y el último al final. Repasa las propiedades en Flexbox.
Ejercicio 7: tarjetas con el botón abajo
Tres tarjetas de rutas de senderismo. Queremos:
- Las tarjetas en fila, con 16px de separación, todas de la misma anchura, y que bajen de línea si no caben (ninguna debe medir menos de 220px).
- Todas con la misma altura, aunque una tenga más texto.
- El botón “Ver ruta” siempre pegado al fondo de su tarjeta, alineado con los de las demás.
<div class="rutas">
<article class="ruta">
<h3>Pico Cordel</h3>
<p>12 km, 900 m de desnivel.</p>
<a class="ruta__boton" href="#">Ver ruta</a>
</article>
<article class="ruta">
<h3>Senda costera</h3>
<p>8 km por los acantilados, con varias playas pequeñas y un faro al final del camino.</p>
<a class="ruta__boton" href="#">Ver ruta</a>
</article>
<article class="ruta">
<h3>Hayedo de Saja</h3>
<p>6 km, ideal en otoño.</p>
<a class="ruta__boton" href="#">Ver ruta</a>
</article>
</div>
Ver solución
.rutas {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.ruta {
flex: 1 1 220px;
display: flex;
flex-direction: column;
padding: 16px;
border: 1px solid #ccc;
border-radius: 8px;
}
.ruta h3 { margin: 0 0 8px; }
.ruta p { margin: 0 0 16px; }
.ruta__boton {
margin-top: auto;
align-self: flex-start;
padding: 8px 16px;
background: #2f6f3e;
color: white;
border-radius: 6px;
text-decoration: none;
}
flex: 1 1 220px significa: parte de 220px, crece para repartir el espacio libre y encoge si hace falta. Con flex-wrap: wrap, cuando no caben tres de 220px, bajan de línea. Las tarjetas ya tienen la misma altura porque align-items vale stretch por defecto. Cada tarjeta es a su vez un flex en columna, y el margin-top: auto del botón absorbe todo el espacio sobrante por encima, empujándolo al fondo. align-self: flex-start evita que el botón se estire a todo el ancho.
Nivel 4: Grid
Ejercicio 8: galería automática
Una galería de fotos de un concurso de fotografía. Con una sola regla de Grid y sin media queries, haz que:
- Haya tantas columnas como quepan, cada una de al menos 180px, repartiéndose el espacio sobrante.
- Las fotos tengan todas proporción cuadrada sin deformarse.
- La primera foto (la ganadora) ocupe dos columnas y dos filas.
<div class="galeria">
<img src="https://picsum.photos/id/10/600/400" alt="Bosque al amanecer">
<img src="https://picsum.photos/id/15/600/400" alt="Cascada">
<img src="https://picsum.photos/id/28/600/400" alt="Lago de montaña">
<img src="https://picsum.photos/id/29/600/400" alt="Cumbre nevada">
<img src="https://picsum.photos/id/42/600/400" alt="Cafetería">
<img src="https://picsum.photos/id/57/600/400" alt="Paseo marítimo">
</div>
Ver solución
.galeria {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 180px), 1fr));
gap: 8px;
}
.galeria img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
display: block;
}
.galeria img:first-child {
grid-column: span 2;
grid-row: span 2;
}
auto-fill con minmax() calcula solo el número de columnas. El min(100%, 180px) impide que una columna sea más ancha que la pantalla en móviles muy estrechos. span 2 hace que la primera foto ocupe dos celdas en cada dirección. Como también es cuadrada, encaja justo en el bloque de 2×2: su lado mide dos columnas más un hueco, igual que dos filas más un hueco. En pantallas donde solo cabe una columna, span 2 crearía una columna extra: si lo notas, mete esa regla en una media query. Más en Grid.
Ejercicio 9: el esqueleto de una página
Maqueta la estructura de la web de un gimnasio con grid-template-areas:
- En móvil, todo en una columna en este orden: cabecera, principal, lateral, pie.
- A partir de 800px: cabecera arriba ocupando todo el ancho; debajo, el principal (que se lleva todo el espacio) y un lateral de 260px a la derecha; y el pie abajo, a todo el ancho.
- La página ocupa al menos todo el alto de la ventana y el pie queda abajo aunque haya poco contenido.
<div class="pagina">
<header class="cabecera">GymNorte</header>
<main class="principal">Horario de clases de esta semana</main>
<aside class="lateral">Oferta: matrícula gratis</aside>
<footer class="pie">© 2026 GymNorte</footer>
</div>
Ver solución
.pagina {
min-height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto auto;
grid-template-areas:
"cabecera"
"principal"
"lateral"
"pie";
gap: 16px;
}
.cabecera { grid-area: cabecera; }
.principal { grid-area: principal; }
.lateral { grid-area: lateral; }
.pie { grid-area: pie; }
.pagina > * {
padding: 16px;
background: #eef1f6;
}
@media (width >= 800px) {
.pagina {
grid-template-columns: 1fr 260px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"cabecera cabecera"
"principal lateral"
"pie pie";
}
}
La fila con 1fr se queda con todo el alto sobrante, así que el pie se va al fondo. 100dvh es el alto de la ventana teniendo en cuenta las barras del navegador del móvil. Para cambiar la distribución en pantalla ancha solo redibujamos el “mapa” de áreas. Tienes más técnicas en Grid a fondo.
Nivel 5: responsive, variables y transiciones
Ejercicio 10: un héroe responsive
La portada de una academia de idiomas. Requisitos:
- El título debe crecer con la pantalla entre
1.75remy3.5remsin media queries. - El texto no debe superar unos 60 caracteres por línea.
- Los dos botones, en columna y a todo el ancho en móvil; en fila y con su ancho natural a partir de 600px.
- Relleno lateral de 16px en móvil y vertical que crezca con la pantalla, entre 32px y 96px.
<section class="heroe">
<h1 class="heroe__titulo">Aprende inglés hablando desde el primer día</h1>
<p class="heroe__texto">Grupos de seis personas como máximo, con profesores nativos y horario de mañana o de tarde.</p>
<div class="heroe__botones">
<a class="boton" href="#">Prueba de nivel gratis</a>
<a class="boton boton--secundario" href="#">Ver horarios</a>
</div>
</section>
Ver solución
.heroe {
padding: clamp(32px, 8vw, 96px) 16px;
}
.heroe__titulo {
font-size: clamp(1.75rem, 1rem + 4vw, 3.5rem);
line-height: 1.1;
margin: 0 0 16px;
}
.heroe__texto {
max-width: 60ch;
margin: 0 0 24px;
}
.heroe__botones {
display: flex;
flex-direction: column;
gap: 12px;
}
.boton {
padding: 12px 20px;
border-radius: 8px;
background: #c2410c;
color: white;
text-align: center;
text-decoration: none;
}
.boton--secundario {
background: transparent;
color: #c2410c;
outline: 2px solid currentColor;
outline-offset: -2px;
}
@media (width >= 600px) {
.heroe__botones {
flex-direction: row;
}
}
clamp(mínimo, preferido, máximo) hace la tipografía fluida: el valor preferido mezcla rem (para respetar el tamaño de letra del usuario) y vw (para crecer con la pantalla). En columna, los hijos de un flex se estiran a todo el ancho; en fila recuperan su ancho natural. La unidad ch es perfecta para limitar la longitud de las líneas. Repasa Responsive y Unidades.
Ejercicio 11: un sistema de botones con variables
Crea botones para una app de reservas usando variables CSS para no repetir código:
- Un botón base
.botoncuyo color sale de una variable--color, con valor por defecto azul (#2563eb). - Dos variantes que solo cambien la variable:
.boton--exito(verde#15803d) y.boton--peligro(rojo#b91c1c). - Una variante
.boton--borde, combinable con las anteriores: fondo transparente, texto y borde del color. - Un modo oscuro: si el sistema está en oscuro, el azul por defecto pasa a ser
#60a5fa.
<button class="boton">Reservar</button>
<button class="boton boton--exito">Confirmar</button>
<button class="boton boton--peligro boton--borde">Cancelar</button>
Ver solución
:root {
color-scheme: light dark;
--azul: light-dark(#2563eb, #60a5fa);
}
.boton {
--color: var(--azul);
padding: 10px 18px;
border: 2px solid var(--color);
border-radius: 8px;
background: var(--color);
color: white;
font: inherit;
cursor: pointer;
}
.boton--exito { --color: #15803d; }
.boton--peligro { --color: #b91c1c; }
.boton--borde {
background: transparent;
color: var(--color);
}
Toda la lógica del color está en .boton; las variantes solo redefinen --color. Como .boton--peligro aparece después de .boton y tiene la misma especificidad, su --color gana. Para el modo oscuro usamos light-dark(), que necesita color-scheme: light dark. Si añades mañana un botón naranja, es una sola línea. Más en Variables CSS y Modo oscuro.
Ejercicio 12: tarjeta con transiciones y tooltip
El ejercicio final junta todo. Una tarjeta de producto de una tienda de deportes:
- Al pasar el ratón o al enfocarla con el teclado (la tarjeta es un enlace), sube 4px y su sombra crece, en 200ms con aceleración suave.
- Una etiqueta “Envío gratis” está oculta y aparece con un fundido y un pequeño desplazamiento hacia arriba al pasar por la tarjeta.
- Solo se animan propiedades que no obliguen a recalcular el diseño.
- Si el usuario ha pedido reducir el movimiento, no hay desplazamientos, solo el cambio de sombra y el fundido.
<a class="producto" href="#">
<span class="producto__etiqueta">Envío gratis</span>
<h3 class="producto__nombre">Zapatillas de trail</h3>
<p class="producto__precio">89,95 €</p>
</a>
Ver solución
.producto {
position: relative;
display: block;
max-width: 280px;
padding: 24px 16px 16px;
border-radius: 12px;
background: white;
color: #1f2937;
text-decoration: none;
box-shadow: 0 2px 6px rgb(0 0 0 / 0.1);
transition: transform 200ms ease, box-shadow 200ms ease;
}
.producto:hover,
.producto:focus-visible {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgb(0 0 0 / 0.18);
}
.producto__etiqueta {
position: absolute;
top: -12px;
right: 12px;
padding: 2px 10px;
border-radius: 999px;
background: #15803d;
color: white;
font-size: 0.8rem;
opacity: 0;
transform: translateY(6px);
transition: opacity 200ms ease, transform 200ms ease;
}
.producto:hover .producto__etiqueta,
.producto:focus-visible .producto__etiqueta {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.producto:hover,
.producto:focus-visible,
.producto__etiqueta,
.producto:hover .producto__etiqueta,
.producto:focus-visible .producto__etiqueta {
transform: none;
}
}
Solo animamos transform, opacity y box-shadow; los dos primeros los mueve la tarjeta gráfica sin recalcular el diseño. La transition se escribe en el estado normal, no en el :hover, para que la animación funcione también al salir. La etiqueta usa position: absolute respecto a la tarjeta, que tiene position: relative. Con prefers-reduced-motion anulamos solo los desplazamientos: la sombra y la opacidad cambian, pero nada se mueve. Repasa Transiciones y Transformaciones.
Errores frecuentes
- Mirar la solución demasiado pronto. Equivocarse y buscar el fallo en DevTools es justo lo que te enseña. Dale al menos un intento serio a cada ejercicio.
- Resolver todo con
!importanto con ids. Funciona hoy y rompe mañana. Si una regla no se aplica, revisa la especificidad en DevTools (las reglas tachadas). - Olvidar el
displaydel padre.gap,justify-contentogrid-template-columnsno hacen nada si el contenedor no esflexogrid. - Probar solo en tu pantalla. Estrecha la ventana hasta 320px en cada ejercicio responsive.
- Escribir la
transitionen el:hover. La animación solo funciona al entrar; al salir, el cambio es brusco. - Usar
widthen lugar demax-widthen tarjetas y contenedores, lo que provoca scroll horizontal en móvil.
Resumen
| Ejercicio | Tema | Lo que practicas |
|---|---|---|
| 1-2 | Selectores | Descendiente, clase, :nth-child, :hover, atributos, ::after |
| 3 | Cascada | Especificidad, style y !important |
| 4-5 | Modelo de caja | box-sizing, margin: auto, max-width |
| 6-7 | Flexbox | space-between, flex-wrap, flex, margin-top: auto |
| 8-9 | Grid | auto-fill, minmax(), span, grid-template-areas |
| 10 | Responsive | Mobile first, clamp(), ch |
| 11 | Variables | Variantes que cambian una variable, light-dark() |
| 12 | Transiciones | transition, transform, opacity, prefers-reduced-motion |
Cuando los tengas todos, el siguiente paso es aprender un framework de utilidades: sigue con Tailwind 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.