CSS Custom Properties: por qué las variables nativas son más poderosas de lo que crees

Artículo que va más allá de la introducción básica a las CSS Custom Properties. En lugar de presentarlas como un simple reemplazo de las variables de Sass, el artículo argumenta que son un mecanismo de comunicación en tiempo de ejecución — entre CSS y JavaScript, entre componentes y su contexto — y muestra patrones concretos que un preprocesador no puede replicar.

Drop me a line

CSS Custom Properties: por qué las variables nativas son más poderosas de lo que crees

Durante años, usar variables en CSS requería un preprocesador. Instalabas Sass o Less, aprendías su sintaxis, configurabas un pipeline de compilación, y a cambio te llevabas $color-primary y $spacing-base. Era un trato razonable.

Hoy ese trato ha cambiado. Las CSS Custom Properties —las variables nativas del lenguaje— no solo reemplazan lo que hacías con Sass: te dan capacidades que ningún preprocesador puede ofrecer. Y la compatibilidad con navegadores ya no es excusa: llevan disponibles desde 2016 y el soporte es universal.


La sintaxis básica

Las custom properties se declaran con el prefijo -- y se consumen con la función var().

:root {
  --color-primary: #3b82f6;
  --spacing-md: 1rem;
  --font-heading: 'Playfair Display', serif;
}

.btn {
  background-color: var(--color-primary);
  padding: var(--spacing-md);
  font-family: var(--font-heading);
}

El selector :root es el equivalente al elemento html pero con mayor especificidad, y es el lugar convencional para definir las variables globales del sistema.

var() también acepta un valor de respaldo por si la variable no está definida:

color: var(--color-accent, #ff6b6b);

Lo que las diferencia de las variables de Sass

Aquí está la diferencia que cambia todo: las custom properties existen en tiempo de ejecución, no en tiempo de compilación.

Las variables de Sass desaparecen una vez compilado el CSS. Son una herramienta de autor, útiles para quien escribe el código pero invisibles para el navegador. Las custom properties, en cambio, viven en el DOM, participan en la cascada y pueden ser leídas y modificadas con JavaScript.

Esto no es un detalle menor. Es lo que hace posibles patrones que con Sass son imposibles.


Por qué usarlas: tres razones que van más allá del DRY

1. Theming sin JavaScript (o casi sin él)

El caso de uso más conocido: el modo oscuro. Con custom properties, basta con redefinir las variables en un contexto alternativo.

:root {
  --bg: #ffffff;
  --text: #111827;
  --surface: #f3f4f6;
}

[data-theme="dark"] {
  --bg: #0f172a;
  --text: #f8fafc;
  --surface: #1e293b;
}
<html data-theme="dark">

Todos los componentes que consumen --bg y --text se adaptan automáticamente. No necesitas dos hojas de estilo, no necesitas clases alternativas en cada componente, no necesitas ninguna lógica especial. El tema se aplica en un único punto y la cascada hace el resto.

Con JavaScript solo necesitas una línea para alternar:

document.documentElement.dataset.theme =
  document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';

2. Las variables participan en la cascada y la herencia

Esta es la característica más subestimada. Una custom property puede ser sobreescrita en cualquier nivel del árbol DOM y todos los descendientes heredarán el nuevo valor.

.card {
  --card-accent: var(--color-primary);
}

.card--warning {
  --card-accent: #f59e0b;
}

.card__badge {
  background: var(--card-accent);
  border: 2px solid var(--card-accent);
}

Aquí card__badge no necesita saber si está dentro de una tarjeta normal o de advertencia. Solo consume --card-accent, y el valor correcto llega por herencia. El componente queda desacoplado de su variante.

Este patrón permite crear componentes verdaderamente agnósticos de su contexto, algo que con variables de Sass es estructuralmente imposible porque los valores se resuelven antes de que el DOM exista.

3. Valores dinámicos con JavaScript

Como las custom properties son parte del DOM, puedes leerlas y escribirlas en tiempo real:

// Leer
const valor = getComputedStyle(document.documentElement)
  .getPropertyValue('--spacing-md');

// Escribir
document.documentElement.style.setProperty('--spacing-md', '1.5rem');

// En un elemento específico
elemento.style.setProperty('--card-accent', '#10b981');

Esto abre la puerta a interacciones que de otra forma requerirían manipulación directa de estilos inline o clases dinámicas: sliders que cambian el tamaño de fuente en tiempo real, efectos de parallax que actualizan una variable con la posición del scroll, themes personalizados por usuario guardados en localStorage.

// Parallax con custom property
window.addEventListener('scroll', () => {
  document.documentElement.style.setProperty(
    '--scroll-y',
    `${window.scrollY}px`
  );
});
.hero__bg {
  transform: translateY(calc(var(--scroll-y) * 0.4));
}

Patrones útiles para sistemas de diseño

Tokens semánticos sobre tokens primitivos

Un patrón muy robusto es definir dos capas de variables: las primitivas (los valores concretos) y las semánticas (el significado de esos valores).

/* Capa primitiva */
:root {
  --blue-500: #3b82f6;
  --blue-600: #2563eb;
  --gray-900: #111827;
  --gray-100: #f3f4f6;
}

/* Capa semántica */
:root {
  --color-interactive: var(--blue-500);
  --color-interactive-hover: var(--blue-600);
  --color-background: var(--gray-100);
  --color-text-primary: var(--gray-900);
}

Cuando cambias de azul a verde en tu sistema de diseño, solo tocas una línea en la capa primitiva. La capa semántica y todos los componentes que la consumen no necesitan cambiar.

Variables como argumentos de componente

.avatar {
  --avatar-size: 2.5rem;
  --avatar-border: none;

  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  border: var(--avatar-border);
}

.avatar--lg {
  --avatar-size: 4rem;
}

.avatar--bordered {
  --avatar-border: 2px solid var(--color-interactive);
}

El componente se configura a través de sus propias variables. Cualquier consumidor puede sobreescribirlas sin necesidad de modificadores adicionales.


Lo que las custom properties no reemplazan

Sass todavía tiene sentido para funciones matemáticas complejas, mixins, bucles que generan código, y condicionales en tiempo de compilación. Si tu proyecto los usa intensivamente, ambas herramientas conviven bien: defines las custom properties en Sass y las sirves como CSS estático.

Para la mayoría de los proyectos modernos, sin embargo, las custom properties junto con calc(), clamp(), y las unidades relativas cubren prácticamente todo lo que un preprocesador aportaba.


Conclusión

Las CSS Custom Properties no son solo “variables nativas”. Son un mecanismo de comunicación entre el CSS y el JavaScript, entre los componentes y su contexto, entre los estados de la interfaz y su presentación visual. Cuando las tratas como lo que son —valores que viven en el DOM y participan en la cascada— empiezan a resolverse problemas de diseño que antes requerían arquitecturas mucho más complejas.

Si aún las usas solo como sustituto de las variables de Sass, te estás perdiendo lo mejor.