Capítulo 11 — CSS moderno¶
Introducción¶
El CSS que se escribe hoy en producción ya no se parece al de hace cinco años. Nuevas características del lenguaje permiten escribir estilos más expresivos, mantenibles y potentes sin necesidad de preprocesadores ni frameworks.
Este capítulo repasa las técnicas que todo desarrollador profesional debería dominar en 2026.
Variables CSS (custom properties)¶
A diferencia de las variables de SCSS, las custom properties existen en tiempo de ejecución y se pueden modificar con JavaScript o con media queries.
:root {
--color-primary: #6a5acd;
--color-text: #1e293b;
--space-sm: 0.5rem;
--space-md: 1rem;
--radius: 12px;
}
.button {
background: var(--color-primary);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius);
}
Cambiar tema con variables¶
:root {
--bg: #ffffff;
--text: #1e293b;
}
[data-theme="dark"] {
--bg: #0f172a;
--text: #e2e8f0;
}
body {
background: var(--bg);
color: var(--text);
}
Con JavaScript, cambiar el tema es solo:
document.documentElement.dataset.theme = 'dark';
Fallback de variables
Puedes dar un valor por defecto: var(--color, #333). Útil cuando la variable puede no estar definida.
Funciones modernas: clamp(), min() y max()¶
Permiten escribir tipografía y espaciados fluidos sin media queries.
/* Título que escala entre 1.5rem y 3rem según el viewport */
h1 {
font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
}
/* Ancho que nunca supera los 800px */
.container {
width: min(100% - 2rem, 800px);
margin-inline: auto;
}
/* Padding mínimo garantizado */
.card {
padding: max(1rem, 2vw);
}
Selectores :is() y :where()¶
Simplifican listas largas de selectores y mejoran la legibilidad.
/* Antes */
article h1, article h2, article h3 { margin-top: 0; }
/* Ahora */
article :is(h1, h2, h3) { margin-top: 0; }
:where() hace lo mismo, pero con especificidad cero. Perfecto para resets:
:where(ul, ol) {
list-style: none;
padding: 0;
}
El selector :has() — el "selector padre"¶
:has() permite estilizar un elemento en función de sus hijos. Es una de las novedades más potentes de los últimos años.
/* Tarjeta con imagen */
.card:has(img) {
grid-template-rows: auto 1fr;
}
/* Formulario con error */
.form:has(input:invalid) {
border-color: crimson;
}
/* Menú con submenú abierto */
.nav-item:has(> .submenu:hover) {
background: #f1f5f9;
}
Soporte
:has() está soportado en todos los navegadores modernos desde 2023. Puedes usarlo en producción sin problema.
Container queries¶
Antes, el diseño responsive dependía del tamaño del viewport. Con @container, un componente responde al tamaño de su contenedor.
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
gap: 1rem;
}
/* Cuando el contenedor supera los 400px */
@container card (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
Esto permite componentes verdaderamente reutilizables en cualquier contexto.
Cascade layers (@layer)¶
Controlan el orden de precedencia de los estilos sin depender de la especificidad.
@layer reset, base, components, utilities;
@layer reset {
* { box-sizing: border-box; }
}
@layer base {
body { font-family: system-ui; }
}
@layer utilities {
.text-center { text-align: center; }
}
El orden de las capas importa más que la especificidad dentro de ellas. Ideal para trabajar con librerías externas sin pelearse con sus estilos.
@scope¶
Limita el alcance de los estilos a un subárbol del DOM.
@scope (.card) to (.card-footer) {
img { border-radius: 12px; }
p { color: #475569; }
}
Los estilos solo se aplican dentro de .card y no dentro de .card-footer.
Subgrid¶
Hereda la cuadrícula del elemento padre, algo imposible con grid normal.
.parent {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.child {
display: grid;
grid-column: span 3;
grid-template-columns: subgrid;
}
Perfecto para alinear contenido entre tarjetas.
Colores modernos: oklch() y color-mix()¶
:root {
--brand: oklch(60% 0.18 280);
--brand-light: color-mix(in oklch, var(--brand) 30%, white);
--brand-dark: color-mix(in oklch, var(--brand) 70%, black);
}
oklch() ofrece percepción uniforme del color, ideal para generar paletas consistentes.
Propiedades lógicas¶
Sustituyen left/right/top/bottom por inline-start, inline-end, block-start, block-end. Esencial para internacionalización (RTL, árabe, hebreo).
.card {
margin-inline: auto;
padding-block: 1rem;
border-inline-start: 2px solid var(--brand);
}
aspect-ratio¶
Antes se usaba el truco del padding-top: 56.25%. Ahora:
.video {
aspect-ratio: 16 / 9;
width: 100%;
}
Caso real: tarjeta de producto responsive¶
<article class="product">
<img src="camiseta.jpg" alt="Camiseta">
<div class="product-info">
<h3>Camiseta</h3>
<p class="price">29 €</p>
<button>Añadir al carrito</button>
</div>
</article>
.product {
container-type: inline-size;
display: grid;
gap: 1rem;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 1rem;
background: var(--bg, white);
}
.product:has(.price:empty) {
opacity: 0.5;
}
@container (min-width: 400px) {
.product {
grid-template-columns: 120px 1fr;
align-items: center;
}
}
.price {
font-weight: 700;
color: var(--brand, #6a5acd);
}
Buenas prácticas¶
- Usa variables CSS para todo lo que se repita: colores, espaciados, radios, tipografías.
- Combina
:is()y:where()para reducir ruido en los selectores. - Prefiere
container queriesa media queries cuando diseñes componentes. - Organiza con
@layerpara separar resets, base, componentes y utilidades. - Usa
clamp()para tipografía fluida ymin()/max()para tamaños con límites. - Adopta propiedades lógicas desde el principio; evita refactorizar después.
- Comprueba compatibilidad en caniuse.com antes de usar features muy nuevas.
Ejercicios¶
- Crea un sistema de temas (claro/oscuro) usando solo custom properties.
- Diseña una tarjeta que cambie de layout según el ancho de su contenedor con
@container. - Usa
:has()para resaltar un formulario cuando tiene un campo inválido. - Implementa un título con tipografía fluida usando
clamp(). - Organiza tus estilos con
@layeren cuatro capas: reset, base, components y utilities.
⬅️ Capítulo 10 — Proyecto final | ➡️ Capítulo 12 — JavaScript asíncrono (pendiente)python3 -c "print(open('docs/capitulos/11-css-moderno.md').read()[:200])"