Variables CSS
Las Custom Properties (más conocidas como "variables CSS") son una de las características más poderosas del lenguaje. Permiten definir valores reutilizables que se propagan por cascada, se heredan entre elementos y se pueden modificar dinámicamente con JavaScript. Son la columna vertebral de cualquier sistema de diseño moderno: sin variables, cambiar un color base en un proyecto de cientos de archivos CSS sería una pesadilla. Con ellas, un solo cambio en :root actualiza toda tu interfaz al instante.
Custom Properties (definición)
Una custom property se define con el prefijo -- seguido de un nombre descriptivo, y se le asigna un valor como cualquier otra propiedad CSS. La diferencia fundamental con las variables de preprocesadores (Sass $var, Less @var) es que las custom properties viven en el navegador: se evalúan en tiempo real, se heredan por cascada y pueden ser leídas o modificadas por JavaScript sin necesidad de recompilar nada. Además, son sensibles a media queries, pseudo-clases y al estado de los elementos, algo imposible en preprocesadores.
El lugar más común para definir variables globales es :root, que es el pseudo-elemento que representa el elemento raíz del documento (<html>). Al definir las variables ahí, están disponibles para cualquier elemento del DOM, ya que todos heredan de :root. Sin embargo, también podés definir variables a nivel de componente (en una clase específica) para crear variaciones locales que sobreescriben las globales, manteniendo la flexibilidad del sistema.
/* Definicion global: disponibles en todo el documento */
:root {
/* Colores del sistema */
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-danger: #ef4444;
/* Fondos */
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
/* Texto */
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #484f58;
/* Tipografia */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--font-heading: 'Space Grotesk', sans-serif;
/* Espaciado */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
/* Bordes */
--border-radius: 8px;
--border-color: #30363d;
/* Sombras */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
/* Transiciones */
--transition-fast: 150ms ease;
--transition-normal: 300ms ease;
--transition-slow: 500ms ease;
}
/* Variables locales: sobreescriben las globales para un componente */
.card {
/* Este componente usa sus propias variables */
--card-padding: var(--space-lg);
--card-bg: var(--bg-secondary);
--card-border: var(--border-color);
--card-radius: var(--border-radius);
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
padding: var(--card-padding);
}
/* Variaciones del mismo componente con solo cambiar variables */
.card--highlight {
--card-bg: #1c2333;
--card-border: var(--color-primary);
/* Hereda todo lo demas de .card, solo sobreescribe lo necesario */
}
.card--compact {
--card-padding: var(--space-sm);
/* Misma card, pero mas compacta */
}
Nomenclatura recomendada
No existe una convención única para nombrar variables CSS, pero el patrón más usado en la industria es --categoria-elemento-modificador (similar a BEM). Ejemplos: --color-primary, --text-secondary, --space-lg, --border-radius-md. Lo importante es ser consistente dentro de tu proyecto. Otra buena práctica es organizar las variables por categoría (colores, tipografía, espaciado, sombras) con comentarios claros, como se ve en el ejemplo de :root arriba.
La función var()
La función var() es la forma de consumir (leer) una custom property. Recibe como primer argumento el nombre de la variable (con el prefijo --) y opcionalmente un segundo argumento que funciona como valor por defecto si la variable no está definida. var() puede usarse en cualquier propiedad CSS—colores, dimensiones, fuentes, sombras, gradientes, incluso dentro de calc(). Esto la hace extremadamente versátil: no estás limitado a variables de color como en algunos frameworks, sino que puedes parameterizar prácticamente cualquier valor visual.
Un detalle importante: var() no se puede usar dentro de los nombres de propiedades ni de selectores (para eso se necesita la función @property o JS). Solo funciona en los valores de las propiedades. Tampoco podés hacer operaciones matemáticas directamente dentro de var()—para eso usá calc() combinado con var().
/* Uso basico de var() */
.btn {
background: var(--color-primary);
color: #fff;
font-family: var(--font-sans);
padding: var(--space-sm) var(--space-lg);
border-radius: var(--border-radius);
transition: all var(--transition-fast);
}
/* var() dentro de calc() */
.sidebar {
width: calc(100% - var(--sidebar-width, 280px));
margin-left: var(--sidebar-width, 280px);
}
/* var() en gradientes */
.hero {
background: linear-gradient(
135deg,
var(--color-primary),
var(--color-secondary)
);
}
/* var() en sombras */
.card {
box-shadow: var(--shadow-md),
0 0 0 1px var(--border-color);
}
/* var() en media queries (esto funciona!) */
.container {
padding: var(--space-md);
}
@media (min-width: 768px) {
.container {
/* Sobreescribis la variable solo en este contexto */
--container-padding: var(--space-xl);
padding: var(--container-padding);
}
}
Fallbacks (valores por defecto)
El segundo parámetro de var() es el fallback: el valor que se usará si la variable no existe o está inicializada con un valor inválido (como initial o un valor vacío). Los fallbacks son esenciales cuando escribis CSS reutilizable (librerías, componentes compartidos, design tokens) porque garantizan que tu componente funcione aunque el usuario no haya definido la variable. El fallback puede incluir a su vez otra llamada a var(), lo que permite crear cadenas de fallback encadenados.
/* Fallback basico: si --color-primary no existe, usa #3b82f6 */
.btn {
background: var(--color-primary, #3b82f6);
}
/* Fallback con variable encadenada:
1ro intenta --component-bg, si no existe
intenta --bg-secondary, si no existe
usa #1a1a2e */
.component {
background: var(--component-bg, var(--bg-secondary, #1a1a2e));
}
/* Fallback en shorthand con comas (separar con coma) */
.text {
/* El fallback "1.5, sans-serif" se pasa completo
a la propiedad font */
font: var(--font-body, 1.5, sans-serif);
}
/* Detalle: si el fallback contiene comas, funciona bien
porque el parser de var() sabe que despues de la primer
coma viene el fallback */
.element {
font-family: var(--font-stack, 'Inter', system-ui, sans-serif);
}
var() no es un preprocesador
A diferencia de las variables de Sass ($var) que se reemplazan en tiempo de compilación, var() se evalúa en tiempo de ejecución por el navegador. Esto significa que si cambias el valor de una variable en :root con JavaScript, todos los elementos que la usan se actualizan instantáneamente sin recargar la página. Es lo que hace posible el theme switching, las animaciones de variables y los design tokens dinámicos.
Cascada, herencia y scope
Las custom properties siguen las mismas reglas de cascada y herencia que cualquier propiedad CSS. Si definís --color: red en un .card, todos los elementos hijos de esa card heredarán --color: red a menos que sobreescribas la variable en un nivel más profundo. Esto es lo que se conoce como "scope" o ámbito: las variables definidas en un elemento son visibles para ese elemento y todos sus descendientes, exactamente igual que funciona la herencia de CSS.
Esta característica es lo que permite crear componentes "tematizables" sin necesidad de clases extra. En lugar de crear .card-blue, .card-green, etc., definís las variables que controlan la apariencia del card y las sobreescribís en el wrapper padre. Cada instancia del componente puede tener su propio "theme" simplemente cambiando el valor de las variables CSS, sin tocar el CSS del componente en sí. Este patrón se conoce como "theming por contenedor" y es extremadamente poderoso para sistemas de diseño escalables.
/* El componente usa variables internas */
.alert {
--alert-bg: var(--bg-secondary);
--alert-border: var(--border-color);
--alert-text: var(--text-primary);
--alert-icon-color: var(--text-secondary);
background: var(--alert-bg);
border-left: 4px solid var(--alert-border);
color: var(--alert-text);
padding: var(--space-md) var(--space-lg);
border-radius: var(--border-radius);
}
.alert .alert-icon {
color: var(--alert-icon-color);
}
/* Sobreescribis las variables en el padre: theming por contenedor */
.alert--success {
--alert-bg: rgba(34, 197, 94, 0.1);
--alert-border: var(--color-success);
--alert-icon-color: var(--color-success);
}
.alert--warning {
--alert-bg: rgba(245, 158, 11, 0.1);
--alert-border: var(--color-warning);
--alert-icon-color: var(--color-warning);
}
.alert--danger {
--alert-bg: rgba(239, 68, 68, 0.1);
--alert-border: var(--color-danger);
--alert-icon-color: var(--color-danger);
}
/* Herencia en accion:
Todos los hijos de .theme-blue heredan estas variables */
.theme-blue {
--accent: #3b82f6;
--accent-light: #60a5fa;
--accent-dark: #2563eb;
}
.theme-blue .btn {
/* .btn hereda --accent de .theme-blue */
background: var(--accent);
}
.theme-blue .link {
color: var(--accent-light);
}
| Característica | Comportamiento | Ejemplo |
|---|---|---|
Definidas en :root |
Globales: disponibles en todo el documento. | :root { --color: #fff; } |
| Definidas en una clase | Locales: solo visibles para el elemento y sus hijos. | .card { --padding: 1rem; } |
| Herencia | Los hijos heredan las variables del padre (igual que color o font-size). |
Si .card define --radius, .card .title puede usarla. |
| Sobrescritura | Un valor más específico (más profundo en el DOM) gana al más general. | .card .btn { --color: red; } sobreescribe el --color de :root. |
initial |
Resetea la variable a su valor heredado, o la elimina si no existe en el scope. | .override { --color: initial; } |
revert |
Revierte la variable al valor del stylesheet del navegador (user-agent). | .reset { --color: revert; } |
Theming: dark / light mode
El caso de uso más popular de las variables CSS es el theming: poder cambiar la apariencia completa de una interfaz (colores, fondos, bordes, sombras) con un solo mecanismo. El patrón clásico es definir las variables de un tema en [data-theme="dark"] y otro en [data-theme="light"] (o directamente en :root para uno y en @media (prefers-color-scheme: light) para el otro). Cuando cambiás el atributo data-theme en el <html>, todas las variables se actualizan instantáneamente y la interfaz entera hace la transición sin recargar.
Este patrón es superior a tener dos archivos CSS separados (uno para dark, otro para light) porque evita la duplicación de reglas: el CSS de tus componentes solo usa var(), y las variables son las únicas que cambian entre temas. También es superior a usar clases como .dark .btn porque no necesitas duplicar selectores ni mantener reglas paralelas. Todo se reduce a un único cambio de contexto.
/* ============================================
TEMA OSCURO (default)
============================================ */
:root,
[data-theme="dark"] {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--border-color: #30363d;
--shadow-color: rgba(0, 0, 0, 0.4);
}
/* ============================================
TEMA CLARO
============================================ */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--text-primary: #1f2328;
--text-secondary: #656d76;
--border-color: #d0d7de;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* ============================================
RESPETA LA PREFERENCIA DEL SISTEMA OPERATIVO
============================================ */
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--text-primary: #1f2328;
--text-secondary: #656d76;
--border-color: #d0d7de;
--shadow-color: rgba(0, 0, 0, 0.1);
}
}
/* ============================================
TRANSICION SUAVE ENTRE TEMAS
============================================ */
*,
*::before,
*::after {
transition: background-color var(--transition-normal),
color var(--transition-normal),
border-color var(--transition-normal),
box-shadow var(--transition-normal);
}
// Toggle de tema con JavaScript
const html = document.documentElement;
const themeToggle = document.getElementById('theme-toggle');
// Recuperar tema guardado (o preferencia del sistema)
function getPreferredTheme() {
const saved = localStorage.getItem('theme');
if (saved) return saved;
return window.matchMedia('(prefers-color-scheme: dark)')
.matches ? 'dark' : 'light';
}
// Aplicar tema
function setTheme(theme) {
html.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
themeToggle.textContent = theme === 'dark'
? '\u2600\uFE0F' // emoji sol
: '\uD83C\uDF19'; // emoji luna
}
// Inicializar
setTheme(getPreferredTheme());
// Toggle al hacer click
themeToggle.addEventListener('click', () => {
const current = html.getAttribute('data-theme');
setTheme(current === 'dark' ? 'light' : 'dark');
});
// Escuchar cambios en la preferencia del sistema
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
});
Guardá la preferencia del usuario
Siempre guardá el tema elegido en localStorage para que se mantenga entre sesiones. Pero también respeta la preferencia del sistema operativo con prefers-color-scheme como fallback si el usuario nunca eligió un tema manualmente. La función getPreferredTheme() del ejemplo arriba implementa exactamente esta lógica: prioridad del usuario guardado > preferencia del sistema > dark como default.
@property: propiedades registradas
La regla @property (CSS Houdini) permite "registrar" una custom property con un tipo de dato, sintaxis y valor inicial. Esto parece un detalle menor, pero tiene una consecuencia enorme: permite animar variables CSS con @keyframes y transition. Sin @property, el navegador trata todas las variables como cadenas de texto y no puede interpolar entre dos valores (por ejemplo, no sabe que #ff0000 y #0000ff son colores para hacer un gradiente). Con @property, le decís al navegador "esta variable siempre es un color" y entonces sí puede calcular los valores intermedios.
Las propiedades registradas con @property también se comportan mejor con initial y inherit, ya que tienen un valor por defecto definido. Esto evita el comportamiento silencioso de las custom properties no registradas, donde una variable no definida simplemente no aplica ningún estilo. La compatibilidad actual es excelente en todos los navegadores modernos (Chrome, Edge, Firefox 128+, Safari 15.4+).
/* Registrar una variable de tipo color */
@property --hue {
syntax: '<number>';
initial-value: 220;
inherits: false;
}
@property --glow-color {
syntax: '<color>';
initial-value: #3b82f6;
inherits: false;
}
@property --card-radius {
syntax: '<length>';
initial-value: 8px;
inherits: true;
}
/* ============================================
ANIMACION DE GRADIENTE con @property
Sin @property, esto NO funcionaria
============================================ */
@property --gradient-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.gradient-animated {
--gradient-angle: 0deg;
background: linear-gradient(
var(--gradient-angle),
var(--color-primary),
var(--color-secondary),
var(--color-primary)
);
background-size: 200% 200%;
animation: rotate-gradient 4s linear infinite;
}
@keyframes rotate-gradient {
to {
--gradient-angle: 360deg;
}
}
/* ============================================
ANIMACION DE COLOR con @property
============================================ */
@property --button-color {
syntax: '<color>';
initial-value: #3b82f6;
inherits: false;
}
.btn-animated {
--button-color: #3b82f6;
background: var(--button-color);
transition: --button-color 0.5s ease;
}
.btn-animated:hover {
--button-color: #8b5cf6;
/* El navegador interpola entre #3b82f6 y #8b5cf6
porque sabe que son colores gracias a @property */
}
/* ============================================
PROGRESS BAR con animacion de color
============================================ */
@property --progress-percent {
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}
.progress-bar {
width: var(--progress-percent);
height: 8px;
background: linear-gradient(90deg, #22c55e, #3b82f6);
border-radius: 4px;
animation: fill-bar 2s ease-out forwards;
}
@keyframes fill-bar {
to {
--progress-percent: 75%;
}
}
| Propiedad de @property | Descripción | Ejemplo |
|---|---|---|
syntax |
El tipo de dato que acepta la variable. Define cómo el navegador la interpreta y si puede animarse. | <color>, <length>, <number>, <angle>, <percentage>, <image>, * (cualquier valor) |
initial-value |
El valor por defecto si la variable no está definida. Debe ser válido según la sintaxis declarada. | initial-value: 0deg;, initial-value: #000; |
inherits |
Si la variable se hereda a los elementos hijos. true para variables globales (colores, fuentes), false para animaciones locales. |
inherits: true; (como color), inherits: false; (como width) |
Sin @property: variables no animables
Las custom properties sin registrar (@property) son tratadas como cadenas de texto por el navegador. Esto significa que transition: --color 0.3s no funciona sin @property, porque el navegador no sabe que --color contiene un color. Si necesitás animar una variable, debés registrarla primero con @property. Para todos los demás usos (theming, valores estáticos, calc()), las variables no registradas funcionan perfectamente.
Variables dinámicas con JavaScript
JavaScript puede leer y escribir custom properties en tiempo real usando la API style de los elementos. Para leer una variable usás getComputedStyle(element).getPropertyValue('--nombre') y para escribir usás element.style.setProperty('--nombre', 'valor'). Esto abre un mundo de posibilidades: animaciones controladas por scroll, configuraciones de usuario que persisten en localStorage, sliders que ajustan espaciado en tiempo real, personalización de colores en un "theme builder", y mucho más.
La ventaja de manipular variables CSS desde JS (en vez de aplicar estilos inline directamente) es que respetás la cascada y la consistencia del sistema de diseño. No estás aplicando element.style.background = '#red' directamente, sino que actualizás la variable que controla el background. Todos los componentes que usan esa variable se actualizan automáticamente, sin que JS necesite conocer cuáles son. Es la diferencia entre "setear un valor" y "configurar un parámetro del sistema".
// =============================================
// LEER una variable CSS desde JavaScript
// =============================================
const root = document.documentElement;
const styles = getComputedStyle(root);
// Lee una variable global (definida en :root)
const primaryColor = styles.getPropertyValue('--color-primary').trim();
console.log(primaryColor); // "#3b82f6"
// Lee una variable de un elemento específico
const card = document.querySelector('.card');
const cardPadding = getComputedStyle(card)
.getPropertyValue('--card-padding').trim();
// =============================================
// ESCRIBIR una variable CSS desde JavaScript
// =============================================
// En :root (variable global)
root.style.setProperty('--color-primary', '#ef4444');
// Todos los elementos que usan var(--color-primary)
// se actualizan instantáneamente
// En un elemento específico (variable local)
card.style.setProperty('--card-padding', '2rem');
// =============================================
// ANIMACION CONTROLADA POR SCROLL
// =============================================
const header = document.querySelector('.site-header');
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
const maxScroll = 300;
const progress = Math.min(scrollY / maxScroll, 1);
// Mapea el scroll al blur del backdrop (0 a 12px)
header.style.setProperty(
'--header-blur',
`${progress * 12}px`
);
// Mapea el scroll a la opacidad del fondo (0.8 a 1)
header.style.setProperty(
'--header-bg-opacity',
0.8 + (progress * 0.2)
);
});
// En el CSS:
// .site-header {
// backdrop-filter: blur(var(--header-blur, 0px));
// background: rgba(13, 17, 23, var(--header-bg-opacity, 0.8));
// }
// =============================================
// SLIDER DE CONFIGURACION EN TIEMPO REAL
// =============================================
const spacingSlider = document.getElementById('spacing-slider');
const previewBox = document.getElementById('preview-box');
spacingSlider.addEventListener('input', (e) => {
const value = e.target.value;
previewBox.style.setProperty('--spacing', `${value}px`);
});
// =============================================
// ELIMINAR una variable CSS
// =============================================
// Remueve la variable del inline style (vuelve al CSS original)
card.style.removeProperty('--card-padding');
Performance: usá requestAnimationFrame
Si actualizás variables CSS dentro de un scroll o mousemove event listener, usá requestAnimationFrame para agrupar las actualizaciones y evitar layout thrashing. Cada llamada a setProperty() dispara un repaint potencial, así que es mejor agrupar todos los cambios en un solo frame de animación. Para animaciones complejas, considerá usar Web Animations API (WAAPI) que permite animar variables CSS registradas con @property de forma más eficiente.