CSS ha cambiado más en los últimos tres años que en la década anterior. Lo que antes requería librerías externas, hacks o JavaScript ahora es posible con CSS puro. Las Container Queries, el subgrid de Grid, las funciones de color de nueva generación, las animaciones basadas en scroll, el anidamiento nativo… hay tanto que aprender que es fácil quedarse atrás.

Esta guía recoge las técnicas de CSS moderno más importantes que todo desarrollador frontend debería dominar en 2026. No es una introducción a CSS básico: asumimos que sabes los fundamentos. Esto es sobre las características que marcan la diferencia en proyectos reales.

En este artículo:

Custom Properties: variables CSS de verdad

Las Custom Properties (también llamadas variables CSS) existen desde hace años pero siguen siendo subestimadas. A diferencia de las variables de preprocesadores como SASS, las custom properties de CSS son en tiempo de ejecución, viven en el DOM y pueden cambiarse con JavaScript. Eso las hace fundamentalmente más poderosas.

Una custom property se declara con dos guiones al inicio del nombre y puede declararse en cualquier selector. Declaradas en :root están disponibles globalmente en todo el documento.

/* Sistema de design tokens con custom properties */
:root {
  /* Colores */
  --color-primario: #3b82f6;
  --color-primario-hover: #2563eb;
  --color-texto: #1e293b;
  --color-fondo: #ffffff;
  --color-borde: #e2e8f0;

  /* Tipografía */
  --fuente-base: 'Inter', system-ui, sans-serif;
  --tamanyo-base: 1rem;
  --tamanyo-sm: 0.875rem;
  --tamanyo-lg: 1.125rem;
  --tamanyo-xl: 1.25rem;
  --peso-normal: 400;
  --peso-medio: 500;
  --peso-bold: 700;

  /* Espaciado */
  --espaciado-1: 0.25rem;
  --espaciado-2: 0.5rem;
  --espaciado-4: 1rem;
  --espaciado-6: 1.5rem;
  --espaciado-8: 2rem;

  /* Bordes */
  --radio-sm: 4px;
  --radio-md: 8px;
  --radio-lg: 12px;
  --radio-full: 9999px;

  /* Sombras */
  --sombra-sm: 0 1px 2px rgba(0,0,0,0.05);
  --sombra-md: 0 4px 6px rgba(0,0,0,0.07);
}

/* Uso */
.btn-primario {
  background: var(--color-primario);
  color: white;
  padding: var(--espaciado-2) var(--espaciado-4);
  border-radius: var(--radio-md);
  font-weight: var(--peso-medio);
}

.btn-primario:hover {
  background: var(--color-primario-hover);
}

Una de las aplicaciones más potentes de las custom properties es implementar temas (modo claro/oscuro) sin JavaScript. Defines dos conjuntos de valores para las mismas variables y cambias el selector que los activa:

:root {
  --bg: #ffffff;
  --texto: #1e293b;
  --superficie: #f8fafc;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --texto: #e2e8f0;
    --superficie: #1e293b;
  }
}

/* Alternativamente, con una clase en el html */
[data-tema="oscuro"] {
  --bg: #0f172a;
  --texto: #e2e8f0;
  --superficie: #1e293b;
}

Container Queries: responsive por componente

Durante años, el diseño responsive en CSS dependía exclusivamente del tamaño de la ventana del navegador con las media queries. El problema es que los componentes modernos no viven en el vacío: una tarjeta de producto puede aparecer en una barra lateral estrecha o en un layout de cuatro columnas, y en cada caso debería verse diferente, pero ambos casos pueden ocurrir en la misma ventana de 1440px.

Container Queries resuelven este problema permitiéndote escribir estilos que responden al tamaño del contenedor del elemento, no al tamaño de la ventana. Es un cambio de paradigma fundamental en cómo diseñamos componentes.

/* Definir el elemento como contenedor de referencia */
.tarjeta-contenedor {
  container-type: inline-size;
  container-name: tarjeta; /* opcional pero recomendado */
}

/* Estilos por defecto (móvil primero) */
.tarjeta {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.tarjeta-imagen {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
}

/* Cuando el CONTENEDOR es ancho, cambiar el layout */
@container tarjeta (min-width: 400px) {
  .tarjeta {
    flex-direction: row;
    align-items: center;
  }

  .tarjeta-imagen {
    width: 160px;
    aspect-ratio: 1;
    border-radius: 8px;
  }
}

/* Cuando el contenedor es muy ancho */
@container tarjeta (min-width: 600px) {
  .tarjeta-imagen {
    width: 240px;
  }
}

Con Container Queries, el mismo componente se adapta automáticamente dependiendo de dónde se coloque en el layout, sin necesidad de variantes adicionales ni lógica JavaScript. Esto hace que los componentes sean verdaderamente reutilizables y autónomos.

CSS Nesting nativo: adiós a la necesidad de SASS

El anidamiento de selectores fue durante mucho tiempo la razón principal por la que los desarrolladores usaban SASS o Less. En 2024, el CSS nativo incorporó el anidamiento de forma oficial, con soporte en todos los navegadores modernos.

CSS Nesting permite escribir reglas anidadas dentro de otras reglas, lo que reduce la repetición de selectores y agrupa el código relacionado de forma más legible:

/* Sin nesting — repetitivo */
.nav { display: flex; }
.nav .nav-lista { list-style: none; gap: 1rem; }
.nav .nav-enlace { color: white; text-decoration: none; }
.nav .nav-enlace:hover { color: #93c5fd; }
.nav .nav-enlace.activo { font-weight: bold; border-bottom: 2px solid currentColor; }

/* Con CSS Nesting nativo — mucho más limpio */
.nav {
  display: flex;
  align-items: center;
  padding: 0 2rem;
  height: 64px;

  & .nav-lista {
    display: flex;
    list-style: none;
    gap: 2rem;
    margin: 0;
    padding: 0;
  }

  & .nav-enlace {
    color: white;
    text-decoration: none;
    font-size: 0.9rem;

    &:hover {
      color: #93c5fd;
    }

    &.activo {
      font-weight: 600;
      border-bottom: 2px solid currentColor;
    }
  }
}

Las pseudoclases :has(), :where() e :is()

Estas tres pseudoclases añaden capacidades de selección que antes eran imposibles en CSS puro o requerían JavaScript.

:has(): conocida como el «selector padre», permite seleccionar un elemento basándose en sus descendientes. Era la feature de CSS más pedida durante años, y finalmente tiene soporte universal en todos los navegadores modernos.

/* Selecciona un label que contiene un input obligatorio */
label:has(input[required]) {
  font-weight: 600;
}
label:has(input[required])::after {
  content: ' *';
  color: red;
}

/* Cambia el layout de la tarjeta si tiene imagen */
.tarjeta:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

/* Oculta el footer vacío */
.card-footer:not(:has(*)) {
  display: none;
}

/* Layout diferente para sección con muchos artículos */
.seccion:has(.articulo:nth-child(4)) {
  grid-template-columns: repeat(4, 1fr);
}

:is(): permite agrupar selectores sin repetir el contexto. Simplifica selectores complejos y también afecta a la especificidad de forma útil.

/* Sin :is() — repetitivo */
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; }

/* Con :is() — limpio */
:is(h1, h2, h3, h4, h5, h6) a { color: inherit; }

:where(): funciona igual que :is() pero con especificidad cero. Útil para estilos base que quieres que sean fáciles de sobrescribir.

Funciones modernas: clamp(), min() y max()

Estas funciones matemáticas permiten hacer tipografía y espaciado verdaderamente fluidos, que se adaptan al tamaño del viewport sin puntos de quiebre arbitrarios.

clamp(mínimo, ideal, máximo): devuelve el valor ideal, pero nunca menor que el mínimo ni mayor que el máximo. Es perfecta para tamaños de fuente responsivos:

/* Tipografía fluida sin media queries */
h1 {
  /* Mínimo 1.75rem, ideal 4vw, máximo 3.5rem */
  font-size: clamp(1.75rem, 4vw, 3.5rem);
}

h2 {
  font-size: clamp(1.5rem, 3vw, 2.5rem);
}

p {
  font-size: clamp(1rem, 1.5vw, 1.125rem);
}

/* Espaciado fluido */
.seccion {
  padding: clamp(2rem, 5vw, 6rem);
}

.grid {
  gap: clamp(1rem, 3vw, 2rem);
}

min(a, b) y max(a, b): devuelven el menor o mayor de los valores. Útiles para crear layouts que se comportan de forma diferente dependiendo del espacio disponible:

/* El contenedor tendrá como máximo 1200px, o el 90% del viewport */
.contenedor {
  width: min(1200px, 90vw);
  margin: 0 auto;
}

/* La imagen nunca será más grande que su contenedor */
img {
  width: min(100%, 600px);
}

CSS Subgrid: alineación perfecta en layouts complejos

Una de las limitaciones históricas de CSS Grid era que los grids anidados no podían alinearse con las columnas del grid padre. Cada grid era un sistema independiente. Subgrid resuelve esto permitiendo que un elemento grid hijo herede las columnas o filas del grid padre.

/* Grid padre con 4 columnas */
.catalogo {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.5rem;
}

/* Cada tarjeta hereda las columnas del padre */
.tarjeta {
  display: grid;
  grid-column: span 1;
  grid-template-rows: subgrid; /* hereda las filas del contexto */
  gap: 0;

  /* Define las filas locales */
  grid-row: span 4; /* ocupa 4 filas del grid padre */
}

/* Ahora las partes internas de cada tarjeta
   se alinean perfectamente entre tarjetas */
.tarjeta-imagen { grid-row: 1; }
.tarjeta-etiqueta { grid-row: 2; }
.tarjeta-titulo { grid-row: 3; }
.tarjeta-footer { grid-row: 4; margin-top: auto; }

El resultado es que en una fila de tarjetas con diferente cantidad de contenido, todos los títulos quedan alineados horizontalmente, todos los footers quedan al mismo nivel, etc. Esto era prácticamente imposible de lograr de forma limpia antes de Subgrid.

Animaciones basadas en scroll

Animar elementos basándose en la posición del scroll era territorio exclusivo de JavaScript. Ahora CSS tiene la propiedad animation-timeline con el valor scroll() que permite vincular el progreso de una animación al desplazamiento de la página:

/* Barra de progreso de lectura */
@keyframes progreso {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

.barra-progreso {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: #3b82f6;
  transform-origin: left;
  animation: progreso linear;
  animation-timeline: scroll(root);
}

/* Animar elementos al entrar en el viewport */
@keyframes aparecer {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.elemento-animado {
  animation: aparecer ease-out both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}

El sistema de color moderno de CSS

CSS ha expandido enormemente sus capacidades de color en los últimos años, con soporte para espacios de color de amplia gama como Display P3 y oklch, que permiten colores más vivos en pantallas modernas.

oklch(): el espacio de color más recomendado actualmente para UI. Es perceptualmente uniforme (los cambios en los valores L, C y H producen cambios visuales consistentes), lo que hace mucho más fácil crear paletas de colores coherentes.

/* oklch(luminosidad chroma tono) */
:root {
  /* Una paleta completa para un color primario usando oklch */
  --azul-100: oklch(95% 0.05 250);
  --azul-200: oklch(88% 0.08 250);
  --azul-300: oklch(79% 0.12 250);
  --azul-400: oklch(68% 0.16 250);
  --azul-500: oklch(58% 0.20 250);  /* color base */
  --azul-600: oklch(48% 0.20 250);
  --azul-700: oklch(39% 0.18 250);
  --azul-800: oklch(30% 0.14 250);
  --azul-900: oklch(22% 0.10 250);
}

/* Usar color-mix para mezclar colores */
.elemento-hover {
  background: color-mix(in oklch, var(--azul-500) 80%, white);
}

/* Colores de amplia gama para pantallas compatibles */
.destaque {
  color: oklch(65% 0.25 30);  /* rojo vibrante en P3 */
}

CSS moderno ha llegado a un punto en el que muchas cosas que antes requerían JavaScript o preprocesadores son ahora posibles directamente en el navegador. La clave para mantenerse al día es tener curiosidad, explorar la documentación de MDN regularmente y experimentar con proyectos reales.

Si quieres profundizar en el diseño de interfaces, te recomendamos leer también nuestra guía sobre CSS Grid vs Flexbox para dominar el layout en CSS, y sobre las 9 funcionalidades esenciales de CSS moderno para completar tu conocimiento del CSS actual.