Tema
CSS moderno
En los últimos años CSS cambió más que en la década anterior. Cosas que antes necesitaban JavaScript, Sass o trucos raros ahora son nativas: componentes que se adaptan a su contenedor, un "selector padre", anidación, capas de cascada, mezclas de color y transiciones entre páginas. En esta lección conocerás las características modernas que ya puedes usar en proyectos reales y cómo dar respaldo a los navegadores que todavía no las soportan.
Concepto
Piensa en esta lección como una caja de herramientas nuevas. No necesitas usarlas todas en cada proyecto, pero conocerlas te permite escribir menos código y resolver problemas que antes parecían imposibles sólo con CSS.
Analogía
Es como cuando tu celular recibe una gran actualización: las apps que usabas siguen funcionando, pero de pronto hay funciones nuevas que te ahorran pasos. Tu CSS de siempre sigue siendo válido; esto lo hace más poderoso.
Container queries
Las media queries miran el ancho de la ventana. Pero un componente (una tarjeta) puede estar en una columna angosta o en un área ancha dentro de la misma pantalla. Las container queries permiten que un componente se adapte al ancho de su contenedor.
html
<div class="tarjeta-envoltura">
<article class="tarjeta">
<img class="tarjeta__imagen" src="img/producto.webp" alt="">
<div class="tarjeta__cuerpo">
<h3>Silla ergonómica</h3>
<p>Comodidad para jornadas largas.</p>
</div>
</article>
</div>css
/* 1. Declara el contenedor que se va a "medir" */
.tarjeta-envoltura {
container-type: inline-size; /* mide su ancho (eje inline) */
container-name: tarjeta; /* nombre opcional */
}
/* 2. Base: tarjeta vertical */
.tarjeta {
display: grid;
gap: 1rem;
}
/* 3. Si el CONTENEDOR mide 420px o más: imagen a la izquierda */
@container tarjeta (min-width: 420px) {
.tarjeta {
grid-template-columns: 160px 1fr;
align-items: center;
}
}
/* Unidades relativas al contenedor: 1cqi = 1 % de su ancho */
.tarjeta h3 {
font-size: clamp(1rem, 4cqi, 1.6rem);
}Mueve el control deslizante y observa cómo la tarjeta cambia de diseño aunque la ventana no cambie:
Buena práctica
Usa media queries para el layout general de la página (columnas, header) y container queries para los componentes reutilizables (tarjetas, widgets). Un mismo componente se verá bien en la barra lateral y en el contenido principal sin clases extra.
Error común
Un elemento no puede consultar su propio tamaño: container-type va en el padre (o envoltura), y las reglas de @container se aplican a sus descendientes.
:has(), el "selector padre"
:has() selecciona un elemento si contiene (o va seguido de) algo que cumpla una condición. Durante años fue el selector más pedido de CSS.
css
/* Tarjeta que contiene una imagen: dos columnas */
.tarjeta:has(img) { grid-template-columns: 120px 1fr; }
/* Campo de formulario cuyo input es inválido (y ya fue editado) */
.campo:has(input:user-invalid) { color: #dc2626; }
/* Tarjeta seleccionable: se resalta cuando su checkbox está marcado */
.opcion:has(input:checked) {
border-color: #7c3aed;
background: #f5f3ff;
}
/* Formulario: desactiva el botón visualmente si hay algún campo inválido */
form:has(:user-invalid) button[type="submit"] { opacity: 0.5; }
/* Bloquear el scroll de la página cuando hay un modal abierto */
body:has(dialog[open]) { overflow: hidden; }
/* Título seguido directamente de un párrafo */
h2:has(+ p) { margin-bottom: 0.25rem; }clamp(), min() y max()
Funciones matemáticas para valores fluidos sin media queries (las viste en Responsive design):
css
h1 {
/* mínimo 2rem, ideal 5vw + 1rem, máximo 3.5rem */
font-size: clamp(2rem, 5vw + 1rem, 3.5rem);
}
.contenedor {
/* el menor de los dos: 100% menos márgenes, o 1200px */
width: min(100% - 2rem, 1200px);
margin-inline: auto;
}
.seccion {
/* al menos 3rem de padding, o 8vh si es mayor */
padding-block: max(3rem, 8vh);
}
/* Grid responsive sin media queries que no se desborda en pantallas diminutas */
.galeria {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr));
gap: 1rem;
}Nesting nativo
Ya puedes anidar en CSS puro, como en Sass:
css
.tarjeta {
padding: 1rem;
& h3 { margin: 0; } /* .tarjeta h3 */
&:hover { translate: 0 -4px; } /* .tarjeta:hover */
&.is-activa { outline: 2px solid; }
.tema-oscuro & { background: #1e1b4b; } /* .tema-oscuro .tarjeta */
@media (min-width: 768px) { /* media query anidada */
padding: 2rem;
}
}Diferencia con Sass
En CSS nativo no puedes concatenar texto: &__titulo no genera .tarjeta__titulo como en Sass. Para BEM con nesting nativo escribe la clase completa o sigue usando Sass.
@layer
Las capas de cascada (viste la idea en Arquitectura CSS) permiten decidir qué grupo de estilos gana, sin pelear con la especificidad:
css
@layer reset, terceros, base, componentes, utilidades;
@import url("libreria.css") layer(terceros);
@layer componentes {
.boton { background: #7c3aed; }
}
@layer utilidades {
.oculto { display: none !important; }
}Regla clave: una capa declarada después gana a las anteriores, sin importar la especificidad. Los estilos sin capa ganan a todas las capas.
aspect-ratio
Proporciones fijas sin el viejo truco del padding-top: 56.25%:
css
.video { aspect-ratio: 16 / 9; width: 100%; }
.avatar { aspect-ratio: 1; width: 64px; border-radius: 50%; object-fit: cover; }
.tarjeta__imagen { aspect-ratio: 4 / 3; object-fit: cover; }Propiedades lógicas
En lugar de left/right/top/bottom, usan el sentido de lectura: inline (horizontal en español) y block (vertical). Así tu sitio funciona también en idiomas de derecha a izquierda (árabe, hebreo).
| Física | Lógica |
|---|---|
margin-left / margin-right | margin-inline-start / margin-inline-end |
margin-left + margin-right | margin-inline |
padding-top + padding-bottom | padding-block |
width / height | inline-size / block-size |
top, right, bottom, left | inset-block-start, inset-inline-end... o inset |
text-align: left | text-align: start |
css
.contenedor { margin-inline: auto; padding-inline: 1rem; }
.seccion { padding-block: 4rem; }
.modal { position: fixed; inset: 0; }color-mix()
Mezcla colores directamente en CSS, ideal para generar variantes desde una variable:
css
:root {
--primario: #7c3aed;
}
.boton {
background: var(--primario);
}
.boton:hover {
background: color-mix(in oklch, var(--primario), black 15%); /* 15 % más oscuro */
}
.etiqueta {
background: color-mix(in srgb, var(--primario) 15%, transparent); /* versión translúcida */
color: var(--primario);
}También existe la sintaxis de color relativo: oklch(from var(--primario) calc(l + 0.1) c h) aclara un color manteniendo su tono.
subgrid
Con subgrid, los hijos de un elemento de grid pueden alinearse con las filas o columnas del grid padre. Resuelve el clásico problema de tarjetas cuyos títulos, textos y botones no quedan alineados entre sí:
css
.tarjetas {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
}
.tarjeta {
display: grid;
grid-row: span 3; /* cada tarjeta ocupa 3 filas del padre */
grid-template-rows: subgrid; /* y usa las filas del padre: título, texto y botón alineados */
gap: 0.75rem;
}@supports: detección de características
Aplica estilos sólo si el navegador soporta algo. Es la base de las estrategias de respaldo:
css
/* Respaldo para todos */
.tarjeta { display: block; }
/* Mejora si soporta container queries */
@supports (container-type: inline-size) {
.envoltura { container-type: inline-size; }
}
/* Soporte de un selector */
@supports selector(:has(a)) {
.opcion:has(input:checked) { border-color: #7c3aed; }
}
@supports not (aspect-ratio: 1) {
.video { height: 0; padding-top: 56.25%; }
}@property
Registra una variable CSS con un tipo. Así el navegador sabe interpolarla y puedes animar cosas que antes no se podían, como un degradado:
css
@property --angulo {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.borde-animado {
border: 3px solid transparent;
border-radius: 1rem;
background:
linear-gradient(#fff, #fff) padding-box,
conic-gradient(from var(--angulo), #7c3aed, #06b6d4, #7c3aed) border-box;
animation: girar 4s linear infinite;
}
@keyframes girar {
to { --angulo: 360deg; } /* sin @property, esto saltaría de golpe */
}View transitions
Animan el cambio entre dos estados de la página (o entre dos páginas) con un efecto de fundido automático. Entre páginas de un sitio multipágina basta con:
css
/* En el CSS de todas las páginas del sitio (mismo origen) */
@view-transition {
navigation: auto;
}
/* Opcional: el logo "viaja" suavemente de una página a otra */
.logo { view-transition-name: logo; }Dentro de una misma página, se usa desde JavaScript:
js
// Si el navegador lo soporta, anima el cambio; si no, simplemente lo aplica
function cambiarVista(actualizar) {
if (!document.startViewTransition) return actualizar()
document.startViewTransition(actualizar)
}Scroll-driven animations (mención)
Animaciones que avanzan con el scroll en lugar del tiempo, sin JavaScript. Por ejemplo, una barra de progreso de lectura:
css
.progreso {
position: fixed;
inset: 0 0 auto;
height: 4px;
background: #7c3aed;
transform-origin: left;
animation: crecer linear;
animation-timeline: scroll(); /* la línea de tiempo es el scroll de la página */
}
@keyframes crecer {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}También existe animation-timeline: view() para animar elementos cuando entran en pantalla. Su soporte todavía no es universal: úsalas como mejora progresiva dentro de @supports (animation-timeline: scroll()) y respeta prefers-reduced-motion.
text-wrap: balance y pretty
css
h1, h2, h3 { text-wrap: balance; } /* reparte las palabras para que las líneas midan parecido */
p { text-wrap: pretty; } /* evita que la última línea quede con una sola palabra */Sin balance, un título puede quedar con una línea larguísima y una palabra huérfana abajo. Si el navegador no lo soporta, simplemente se ignora: es mejora progresiva gratuita.
Soporte de navegadores y estrategias de respaldo
Antes de usar algo nuevo en un proyecto de cliente, consulta caniuse.com (escribe "container queries", ":has" o "subgrid") y la sección de compatibilidad al final de cada página de MDN. Busca también la etiqueta Baseline: "Baseline ampliamente disponible" significa que funciona en todos los navegadores principales desde hace al menos 30 meses.
Estrategias:
Mejora progresiva: escribe una versión base que funcione en todos lados y añade lo moderno encima. Si no se soporta, el usuario ve la versión base (perfectamente usable).
CSS ignora lo que no entiende: declara el respaldo primero y la versión moderna después.
css.hero { min-height: 100vh; /* respaldo */ min-height: 100dvh; /* moderno: respeta las barras del navegador móvil */ }@supportspara bloques que dependen de una característica.Herramientas: Autoprefixer, Lightning CSS o Vite pueden transformar sintaxis moderna (como nesting) en CSS compatible.
| Característica | Estado aproximado (2026) |
|---|---|
Container queries, :has(), nesting, @layer, subgrid | Ampliamente disponible: úsalas |
color-mix(), aspect-ratio, propiedades lógicas, clamp() | Ampliamente disponible |
text-wrap: balance, @property | Disponible en navegadores actuales |
| View transitions entre páginas, scroll-driven animations | Usa como mejora progresiva |
Buena práctica
Pregunta a tu cliente qué navegadores usa su público (o revisa su Google Analytics). Un sistema interno de una empresa con equipos viejos no es lo mismo que una landing para jóvenes con celulares nuevos.
Ejercicios
- Crea un componente "perfil" que muestre la foto arriba cuando su contenedor mide menos de 400px y a la izquierda cuando mide más (usa container queries).
- Con
:has(), haz que una etiquetalabelse ponga en negritas y violeta cuando su checkbox interior está marcado. - Escribe un contenedor centrado con
min()que nunca supere 1100px y deje 1.5rem de margen a cada lado en móvil. - Usando
color-mix(), crea un botón cuyo hover sea 20 % más oscuro que--color-marca. - Reescribe con propiedades lógicas:
margin-left: auto; margin-right: auto; padding-top: 2rem; padding-bottom: 2rem;.
Ver solución
css
/* 1 */
.perfil-envoltura { container-type: inline-size; }
.perfil { display: grid; gap: 1rem; justify-items: center; text-align: center; }
.perfil img { width: 96px; aspect-ratio: 1; border-radius: 50%; object-fit: cover; }
@container (min-width: 400px) {
.perfil {
grid-template-columns: auto 1fr;
justify-items: start;
text-align: start;
align-items: center;
}
}
/* 2 */
label:has(input[type="checkbox"]:checked) {
font-weight: 700;
color: #7c3aed;
}
/* 3 */
.contenedor {
width: min(100% - 3rem, 1100px);
margin-inline: auto;
}
/* 4 */
.boton { background: var(--color-marca); }
.boton:hover { background: color-mix(in oklch, var(--color-marca), black 20%); }
/* 5 */
.bloque {
margin-inline: auto;
padding-block: 2rem;
}Resumen
- Container queries (
container-type,@container,cqi): componentes que se adaptan a su contenedor. :has(): estilos según lo que un elemento contiene; adiós a mucho JavaScript.clamp(),min(),max(): valores fluidos sin media queries.- Nesting nativo y
@layertraen a CSS puro ideas de Sass e ITCSS. aspect-ratio, propiedades lógicas,color-mix(),subgridy@propertysimplifican problemas clásicos.- View transitions, scroll-driven animations y
text-wrapson mejoras progresivas. - Consulta caniuse y Baseline; usa respaldo +
@supportscuando haga falta.
Siguiente: Proyecto final: sitio responsive.