Skip to content

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:

Container queries

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; }
Ejemplos interactivos de :has()

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ísicaLógica
margin-left / margin-rightmargin-inline-start / margin-inline-end
margin-left + margin-rightmargin-inline
padding-top + padding-bottompadding-block
width / heightinline-size / block-size
top, right, bottom, leftinset-block-start, inset-inline-end... o inset
text-align: lefttext-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:

  1. 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).

  2. 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 */
    }
  3. @supports para bloques que dependen de una característica.

  4. Herramientas: Autoprefixer, Lightning CSS o Vite pueden transformar sintaxis moderna (como nesting) en CSS compatible.

CaracterísticaEstado aproximado (2026)
Container queries, :has(), nesting, @layer, subgridAmpliamente disponible: úsalas
color-mix(), aspect-ratio, propiedades lógicas, clamp()Ampliamente disponible
text-wrap: balance, @propertyDisponible en navegadores actuales
View transitions entre páginas, scroll-driven animationsUsa 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 ​

  1. 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).
  2. Con :has(), haz que una etiqueta label se ponga en negritas y violeta cuando su checkbox interior está marcado.
  3. Escribe un contenedor centrado con min() que nunca supere 1100px y deje 1.5rem de margen a cada lado en móvil.
  4. Usando color-mix(), crea un botón cuyo hover sea 20 % más oscuro que --color-marca.
  5. 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 @layer traen a CSS puro ideas de Sass e ITCSS.
  • aspect-ratio, propiedades lógicas, color-mix(), subgrid y @property simplifican problemas clásicos.
  • View transitions, scroll-driven animations y text-wrap son mejoras progresivas.
  • Consulta caniuse y Baseline; usa respaldo + @supports cuando haga falta.

Siguiente: Proyecto final: sitio responsive.

Academia Web — aprende, copia y construye.