Overview
CSS custom properties (variables) te permiten definir design tokens — valores nombrados para colores, spacing, tipografía y otras decisiones de diseño. A diferencia de las variables de preprocesador, las custom properties son live: pueden cambiar en runtime, responder a media queries y cascadear a través del DOM. Esto las hace ideales para theming, responsive design y crear design systems consistentes sin JavaScript o compilación en build time.
When to Use
- Construir un design system con spacing, colores y tipografía consistentes
- Soportar múltiples temas (light/dark, variantes de brand)
- Escalado responsivo — ajustar spacing y font sizes en diferentes breakpoints
- Crear librerías de componentes reutilizables que se adaptan a los tokens de la aplicación host
- Switching de tema en runtime sin page reload
When NOT to Use
- Soporte de browsers legacy — las custom properties no funcionan en IE11
- Valores que nunca cambian y no necesitan theming — un
z-indexhardcodeado está bien - Manipulación compleja de color — usa
color-mix()o funciones de preprocesador para math de color dinámico
Solution
Definiciones básicas de tokens
:root {
/* Escala de color */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-900: #1e3a8a;
/* Escala neutral */
--color-white: #ffffff;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-500: #6b7280;
--color-gray-900: #111827;
--color-black: #000000;
/* Tokens semánticos */
--color-bg: var(--color-white);
--color-text: var(--color-gray-900);
--color-text-muted: var(--color-gray-500);
--color-border: var(--color-gray-200);
--color-accent: var(--color-primary-500);
/* Escala de spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
--space-16: 4rem;
/* Tipografía */
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Radius */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15);
}
Usar tokens en componentes
.card {
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6);
box-shadow: var(--shadow-sm);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--line-height-normal);
}
.card__title {
font-size: var(--text-xl);
font-weight: 600;
margin-bottom: var(--space-2);
}
.card__body {
color: var(--color-text-muted);
font-size: var(--text-sm);
}
Dark theme con tokens semánticos
:root {
--color-bg: var(--color-white);
--color-text: var(--color-gray-900);
--color-text-muted: var(--color-gray-500);
--color-border: var(--color-gray-200);
--color-surface: var(--color-white);
--color-surface-hover: var(--color-gray-100);
}
[data-theme="dark"] {
--color-bg: #0f172a;
--color-text: #e2e8f0;
--color-text-muted: #94a3b8;
--color-border: #1e293b;
--color-surface: #1e293b;
--color-surface-hover: #334155;
}
Toggle theme seteando data-theme en el elemento root:
document.documentElement.setAttribute("data-theme", "dark");
Spacing responsivo con media queries
:root {
--space-page: var(--space-4);
--text-heading: var(--text-xl);
}
@media (min-width: 768px) {
:root {
--space-page: var(--space-8);
--text-heading: var(--text-2xl);
}
}
@media (min-width: 1280px) {
:root {
--space-page: var(--space-12);
--text-heading: var(--text-2xl);
}
}
.page {
padding: var(--space-page);
}
.page__title {
font-size: var(--text-heading);
}
Tipografía directa con clamp
:root {
--text-fluid-sm: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
--text-fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--text-fluid-lg: clamp(1.25rem, 1rem + 1vw, 1.5rem);
--text-fluid-xl: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
--text-fluid-2xl: clamp(2rem, 1.5rem + 2.5vw, 3rem);
}
h1 { font-size: var(--text-fluid-2xl); }
h2 { font-size: var(--text-fluid-xl); }
h3 { font-size: var(--text-fluid-lg); }
p { font-size: var(--text-fluid-base); }
Tokens scoped a componente
.button {
--button-bg: var(--color-primary-500);
--button-text: var(--color-white);
--button-padding-x: var(--space-4);
--button-padding-y: var(--space-2);
--button-radius: var(--radius-md);
background: var(--button-bg);
color: var(--button-text);
padding: var(--button-padding-y) var(--button-padding-x);
border-radius: var(--button-radius);
border: none;
cursor: pointer;
}
.button--secondary {
--button-bg: var(--color-gray-100);
--button-text: var(--color-gray-900);
}
.button--large {
--button-padding-x: var(--space-6);
--button-padding-y: var(--space-3);
}
Escala de z-index
:root {
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
}
.dropdown { z-index: var(--z-dropdown); }
.modal { z-index: var(--z-modal); }
.toast { z-index: var(--z-toast); }
Tokens de transición
:root {
--transition-fast: 150ms ease-out;
--transition-base: 250ms ease-out;
--transition-slow: 400ms ease-out;
}
.button {
transition: background var(--transition-fast), transform var(--transition-fast);
}
.button:hover {
background: var(--color-primary-600);
transform: translateY(-1px);
}
Usar tokens con Tailwind CSS v4
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-900: #1e3a8a;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
}
Tailwind v4 lee estos tokens y genera utilities como bg-brand-500, p-6, rounded-md.
Variants
Usar tokens en JavaScript
function getToken(name) {
return getComputedStyle(document.documentElement)
.getPropertyValue(name)
.trim();
}
const primaryColor = getToken("--color-primary-500");
const spaceUnit = getToken("--space-4");
Switching de tema con localStorage
const theme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", theme);
function toggleTheme() {
const current = document.documentElement.getAttribute("data-theme");
const next = current === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
}
Tokens brand-specific
[data-brand="acme"] {
--color-primary-500: #e11d48;
--color-primary-600: #be123c;
}
[data-brand="globex"] {
--color-primary-500: #059669;
--color-primary-600: #047857;
}
Best Practices
-
For a deeper guide, see Dark Mode with prefers-color-scheme and CSS Variables.
-
Usa nombres semánticos de tokens (
--color-bg,--color-text) en lugar de nombres de color crudos (--blue-500) — los tokens semánticos hacen el theming trivial -
Define escalas de color crudas separadas de los tokens semánticos — las escalas crudas son la paleta, los tokens semánticos son el significado
-
Mantén la escala de spacing consistente — usa una unidad base (0.25rem) y multiplica
-
Usa
clamp()para tipografía directa — se adapta suavemente a través de tamaños de pantalla -
Agrupa tokens por categoría (colors, spacing, typography, shadows) con comentarios
-
Usa tokens scoped a componente para componentes complejos — overridea tokens parent localmente
-
Testea ambos temas durante el desarrollo — no construyas toda la UI en light mode y luego intentes agregar dark mode
Common Mistakes
- Usar valores de color crudos en componentes:
color: #3b82f6en lugar decolor: var(--color-accent)— el theming se vuelve imposible. - Hardcodear spacing:
padding: 14pxen lugar depadding: var(--space-3)— spacing inconsistente a través de la app. - No definir tokens semánticos: mapear componentes directamente a tokens de escala cruda (
--color-primary-500) hace que los cambios de tema requieran actualizar cada componente. - Overridear tokens a nivel de componente sin scoping: setear
--color-bgglobalmente dentro de un rule.cardafecta a todos los descendientes inesperadamente. - No testear dark theme: algunas combinaciones de tokens se ven bien en light mode pero tienen contraste pobre en dark mode.
Preguntas frecuentes
¿Cuál es la diferencia entre raw tokens y semantic tokens?
Los raw tokens son la paleta (--color-blue-500: #3b82f6). Los semantic tokens asignan significado (--color-accent: var(--color-blue-500)). Los componentes usan tokens semánticos. Para cambiar el color accent, actualizas el token semántico, no cada componente.
¿Puedo usar custom properties con preprocesadores?
Sí. Las variables de Sass/LESS son compile-time; las custom properties son runtime. Puedes usar ambas — Sass para valores estáticos, custom properties para valores themeable.
¿Cómo soporto IE11?
No puedes usar custom properties en IE11. Usa @supports para proveer fallbacks:
.card { background: #ffffff; }
@supports (--css: variables) {
.card { background: var(--color-bg); }
}
¿Cómo cascadean las custom properties?
Como las CSS properties regulares. Un token definido en :root aplica a todos los elementos. Un token definido en .dark-theme overridea el valor de :root para elementos dentro de .dark-theme.
¿Debería usar custom properties o @theme de Tailwind?
@theme de Tailwind v4 usa custom properties por debajo. Usa @theme si estás usando Tailwind — genera utilities desde tus tokens. Usa custom properties crudas si no estás usando Tailwind.
Recursos Relacionados
Container Queries para Responsiveness de Componentes
Cómo usar CSS container queries para layouts responsivos a nivel de componente que se adaptan al tamaño de su contenedor en lugar del viewport.
RecipeDark Mode con prefers-color-scheme y CSS Variables
Cómo implementar dark mode usando el media query CSS prefers-color-scheme, CSS custom properties y toggle manual con persistencia en localStorage.
RecipeType Checking Exhaustivo con Discriminated Unions
Cómo usar discriminated unions de TypeScript para type checking exhaustivo, asegurando que todos los casos se manejen en compile time con aserciones de tipo never.
GuideAccesibilidad Web: WCAG 2.2
Dominá accesibilidad web con WCAG 2.2: roles ARIA, navegación por teclado, soporte para screen readers, contraste de color y formularios accesibles.