Tutoriales

Propiedades Personalizadas CSS para Sistemas de Color Dinámicos

8 min de lectura

Las propiedades personalizadas CSS — comúnmente llamadas variables CSS — son la base de cualquier sistema de color moderno y mantenible. Te permiten definir un color una vez y referenciarlo en todas partes, convirtiendo los cambios de tema global en una edición de una sola línea en lugar de un buscar-y-reemplazar en cientos de archivos. Combinadas con la cascada y JavaScript, desbloquean capacidades de tematización dinámica que los preprocesadores como Sass o Less simplemente no pueden igualar.

Este tutorial recorre la construcción de un sistema de color listo para producción desde cero usando propiedades personalizadas CSS: desde los fundamentos hasta el cambio de modo claro/oscuro, las sobrescrituras a nivel de componente y la manipulación de color en tiempo de ejecución con JavaScript.

Fundamentos de las Variables CSS

Una propiedad personalizada CSS es cualquier propiedad cuyo nombre comienza con dos guiones (--). La declaras como cualquier otra propiedad y la lees con la función var().

:root {
  --color-brand: #2563EB;
}

.button {
  background-color: var(--color-brand);
}

El selector :root es equivalente al elemento html pero con mayor especificidad, lo que lo convierte en el lugar convencional para los tokens globales. Cualquier elemento del documento puede leer --color-brand.

Valores de Fallback

La función var() acepta un fallback opcional, usado cuando la variable es indefinida o inválida:

.card {
  /* Recurre a #6B7280 si --color-secondary no está definido */
  color: var(--color-secondary, #6B7280);
}

Los fallbacks pueden anidarse — el segundo argumento de var() puede usar var() en sí mismo — lo cual es útil para sistemas de tokens de diseño en capas.

Propiedades Personalizadas vs. Variables de Preprocesador

Las variables de Sass y Less se resuelven en tiempo de compilación y se integran en el CSS generado. Una vez compiladas, desaparecen. Las propiedades personalizadas CSS viven en el navegador y responden a la cascada, la herencia y JavaScript. Esta distinción permite todo lo que sigue en este tutorial.

Arquitectura del Tema de Color

Un sistema de tokens bien diseñado tiene al menos dos capas: tokens primitivos (valores crudos) y tokens semánticos (referencias orientadas a propósito).

Tokens Primitivos: La Paleta Completa

Los tokens primitivos definen cada color que tu sistema puede usar. La mejor práctica es generar una escala de sombras completa para cada tono que necesites. Usa el Generador de Sombras para producir una escala estilo Tailwind 50–950 para cualquier color base.

Por ejemplo, empezando desde un azul de marca #2563EB:

:root {
  /* Escala azul — generada desde #2563EB */
  --blue-50:  #EFF6FF;
  --blue-100: #DBEAFE;
  --blue-200: #BFDBFE;
  --blue-300: #93C5FD;
  --blue-400: #60A5FA;
  --blue-500: #3B82F6;
  --blue-600: #2563EB;
  --blue-700: #1D4ED8;
  --blue-800: #1E40AF;
  --blue-900: #1E3A8A;
  --blue-950: #172554;

  /* Escala neutral */
  --gray-50:  #F9FAFB;
  --gray-100: #F3F4F6;
  --gray-200: #E5E7EB;
  --gray-300: #D1D5DB;
  --gray-400: #9CA3AF;
  --gray-500: #6B7280;
  --gray-600: #4B5563;
  --gray-700: #374151;
  --gray-800: #1F2937;
  --gray-900: #111827;
  --gray-950: #030712;
}

Los tokens primitivos nunca se usan directamente en los estilos de componentes. Existen únicamente para ser referenciados por tokens semánticos.

Tokens Semánticos: Intención sobre Valor

Los tokens semánticos dan significado a los valores crudos. En lugar de --blue-600, tu componente usa --color-action-primary. Esta abstracción te permite reasignar más tarde --color-action-primary a un verde o morado y que cada componente que lo use se actualice automáticamente.

:root {
  /* Fondo */
  --color-bg-base:       var(--gray-50);
  --color-bg-surface:    #FFFFFF;
  --color-bg-elevated:   #FFFFFF;
  --color-bg-subtle:     var(--gray-100);

  /* Texto */
  --color-text-primary:  var(--gray-900);
  --color-text-secondary: var(--gray-600);
  --color-text-disabled: var(--gray-400);
  --color-text-inverse:  #FFFFFF;

  /* Marca / Acción */
  --color-action-primary:       var(--blue-600);
  --color-action-primary-hover: var(--blue-700);
  --color-action-primary-text:  #FFFFFF;

  /* Bordes */
  --color-border-base:   var(--gray-200);
  --color-border-strong: var(--gray-400);

  /* Retroalimentación */
  --color-success: #16A34A;
  --color-warning: #D97706;
  --color-error:   #DC2626;
  --color-info:    var(--blue-600);
}

Los estilos de componentes referencian exclusivamente tokens semánticos:

.button-primary {
  background-color: var(--color-action-primary);
  color:            var(--color-action-primary-text);
  border:           1px solid var(--color-action-primary);
}

.button-primary:hover {
  background-color: var(--color-action-primary-hover);
}

.card {
  background-color: var(--color-bg-surface);
  border:           1px solid var(--color-border-base);
  color:            var(--color-text-primary);
}

Esta arquitectura de dos capas mantiene el sistema flexible y refactorizable. Si tu color de marca cambia, actualizas un token primitivo y la capa semántica propaga el cambio por todas partes.

Patrón de Alternancia Claro/Oscuro

Las propiedades personalizadas CSS se heredan a través del árbol del documento, lo que significa que puedes redefinir un token en un ámbito específico y cada descendiente hereda el nuevo valor. Este es el mecanismo que hace que la tematización claro/oscuro sea trivial.

Enfoque 1: prefers-color-scheme (Automático)

La implementación más simple responde a la preferencia del sistema operativo del usuario:

:root {
  /* Modo claro (predeterminado) */
  --color-bg-base:      #FFFFFF;
  --color-bg-surface:   #F9FAFB;
  --color-text-primary: #111827;
  --color-text-secondary: #6B7280;
  --color-border-base:  #E5E7EB;
  --color-action-primary: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    /* Sobrescrituras del modo oscuro */
    --color-bg-base:      #111827;
    --color-bg-surface:   #1F2937;
    --color-text-primary: #F9FAFB;
    --color-text-secondary: #9CA3AF;
    --color-border-base:  #374151;
    --color-action-primary: #3B82F6;
  }
}

Cada componente obtiene automáticamente los colores correctos. No se requiere JavaScript. El CSS del componente no cambia en absoluto.

Enfoque 2: Alternancia por Atributo de Datos (Manual)

Para el cambio de tema controlado por el usuario, el patrón común es un atributo data-theme en el elemento html o body:

:root,
[data-theme="light"] {
  --color-bg-base:      #FFFFFF;
  --color-text-primary: #111827;
  --color-action-primary: #2563EB;
}

[data-theme="dark"] {
  --color-bg-base:      #111827;
  --color-text-primary: #F9FAFB;
  --color-action-primary: #60A5FA;
}

Observa que el modo oscuro usa #60A5FA (blue-400) en lugar de #2563EB (blue-600) para el color de acción primaria. Sobre fondos oscuros, las sombras más claras de azul mantienen un contraste accesible. Usa el Verificador de Contraste para verificar que cada par de token semántico (texto sobre fondo) pase WCAG AA en ambos modos.

Alterna el tema con una pequeña función JavaScript:

function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('color-theme', theme);
}

// Al cargar la página, respetar la preferencia guardada
const saved = localStorage.getItem('color-theme');
if (saved) {
  setTheme(saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  setTheme('dark');
}

// Botón de alternancia
document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  setTheme(current === 'dark' ? 'light' : 'dark');
});

Prevención del Destello de Contenido Sin Estilo

Al usar localStorage para la persistencia del tema, coloca un script en línea bloqueante en el <head> — antes de cualquier hoja de estilos — para aplicar el atributo de tema guardado antes de que el navegador pinte:

<head>
  <script>
    const theme = localStorage.getItem('color-theme') ||
      (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', theme);
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

Este pequeño script en línea asegura que el tema correcto se aplique antes de que se analice cualquier CSS, eliminando la transición de destello-blanco-luego-oscuro que afecta a los modos oscuros implementados deficientemente.

Sobrescrituras de Color a Nivel de Componente

Una de las características más poderosas de las propiedades personalizadas CSS son las sobrescrituras de ámbito — redefinir un token dentro del ámbito de un componente específico para que sus hijos hereden el valor local.

Caso de Uso: Sección Hero Invertida (Oscura)

Supón que tienes un token semántico global --color-bg-base establecido en blanco, y quieres una sección hero oscura donde todo el texto se adapte automáticamente:

.hero--dark {
  --color-bg-base:        #111827;
  --color-text-primary:   #F9FAFB;
  --color-text-secondary: #9CA3AF;
  --color-border-base:    #374151;

  background-color: var(--color-bg-base);
  color: var(--color-text-primary);
}

/* Sin cambios necesarios — .hero__title hereda los valores sobrescritos */
.hero__title {
  color: var(--color-text-primary);
}

.hero__subtitle {
  color: var(--color-text-secondary);
}

El modificador .hero--dark redefine los tokens localmente. Cada elemento dentro de .hero--dark que use esos tokens hereda los valores oscuros automáticamente, sin duplicación de nombres de clase como hero__title--dark.

Caso de Uso: Variantes de Alerta de Marca

Las sobrescrituras a nivel de componente son ideales para variantes semánticas:

.alert {
  background-color: var(--alert-bg, var(--color-bg-subtle));
  color:            var(--alert-text, var(--color-text-primary));
  border-left:      4px solid var(--alert-accent, var(--color-border-strong));
  padding:          1rem 1.25rem;
  border-radius:    0.375rem;
}

.alert--success {
  --alert-bg:     #F0FDF4;
  --alert-text:   #14532D;
  --alert-accent: #16A34A;
}

.alert--error {
  --alert-bg:     #FEF2F2;
  --alert-text:   #7F1D1D;
  --alert-accent: #DC2626;
}

.alert--warning {
  --alert-bg:     #FFFBEB;
  --alert-text:   #78350F;
  --alert-accent: #D97706;
}

El componente base .alert hace referencia a variables privadas a nivel de componente (--alert-bg, --alert-text, --alert-accent) con fallbacks a tokens globales. Los modificadores de variante establecen esas variables privadas. Este patrón mantiene el CSS del componente base limpio y hace que agregar nuevas variantes sea trivial.

Cambios de Color en Tiempo de Ejecución con JavaScript

Dado que las propiedades personalizadas CSS están activas en el navegador, JavaScript puede leerlas y escribirlas en cualquier momento — habilitando sistemas de color dinámicos que responden a la entrada del usuario, datos o estado de la aplicación.

Leer y Escribir Propiedades Personalizadas

const root = document.documentElement;

// Leer una propiedad personalizada
const brandColor = getComputedStyle(root).getPropertyValue('--color-action-primary').trim();
console.log(brandColor); // '#2563EB'

// Establecer una propiedad personalizada
root.style.setProperty('--color-action-primary', '#7C3AED');

// Eliminar una sobrescritura (revierte al valor de la hoja de estilos)
root.style.removeProperty('--color-action-primary');

Caso de Uso: Color de Marca Personalizable por el Usuario

Supón que estás construyendo un panel de control SaaS donde cada inquilino puede establecer su color de marca. Recibes el color de marca de una API y lo aplicas en tiempo de ejecución:

async function applyTenantTheme(brandHex) {
  // brandHex: '#7C3AED' (un morado)
  const root = document.documentElement;

  // Establecer el color de acción primaria
  root.style.setProperty('--color-action-primary', brandHex);

  // Derivar una variante de hover (calculada en el servidor o mediante una pequeña biblioteca)
  const hoverHex = darkenColor(brandHex, 0.1);
  root.style.setProperty('--color-action-primary-hover', hoverHex);
}

El Conversor de Color puede ayudarte a determinar qué valores OKLCH o HSL corresponden a cualquier código HEX, lo que es útil al construir lógica de derivación de sombras. Por ejemplo, #7C3AED en OKLCH es aproximadamente oklch(0.52 0.23 295) — puedes derivar variantes más claras y más oscuras ajustando el componente L mientras mantienes constantes C y H.

Caso de Uso: Selector de Tema de Color en Tiempo Real

<input type="color" id="brand-picker" value="#2563EB">
document.getElementById('brand-picker').addEventListener('input', (e) => {
  document.documentElement.style.setProperty('--color-action-primary', e.target.value);
});

Este es un editor de temas en vivo genuino en doce líneas de código. Sin paso de compilación, sin biblioteca, sin ciclo de re-renderizado. El navegador actualiza cada elemento que usa --color-action-primary instantáneamente a medida que el usuario arrastra el selector de color.

Animando Propiedades Personalizadas

Las propiedades personalizadas también pueden interpolarse usando @property, la API de Houdini que te permite declarar el tipo de una propiedad y su valor inicial:

@property --gradient-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.animated-border {
  --gradient-angle: 0deg;
  background: conic-gradient(
    from var(--gradient-angle),
    #2563EB, #7C3AED, #EC4899, #2563EB
  );
  animation: rotate 4s linear infinite;
}

@keyframes rotate {
  to { --gradient-angle: 360deg; }
}

Sin @property, el navegador no sabe que --gradient-angle es un ángulo y no puede interpolarlo. Con él, la animación se ejecuta sin problemas.

Conclusiones Clave

  • Las propiedades personalizadas CSS son valores activos, no sustituciones en tiempo de compilación. La cascada, la herencia y JavaScript son aplicables — dándote capacidades que ningún sistema de variables de preprocesador puede igualar.
  • Usa una arquitectura de token de dos capas: tokens primitivos (valores de color crudos) y tokens semánticos (referencias orientadas a propósito). Los componentes referencian únicamente tokens semánticos. Esto hace que la refactorización a gran escala sea un cambio de una sola línea.
  • El modo claro/oscuro es solo una redefinición de token: usa @media (prefers-color-scheme: dark) para el cambio automático o un atributo data-theme para el control manual. Los componentes nunca necesitan CSS específico de variante.
  • Las sobrescrituras a nivel de componente te permiten crear secciones invertidas y variantes semánticas redefiniendo tokens en un ámbito local — no se necesitan nombres de clase adicionales ni CSS duplicado.
  • JavaScript puede leer y escribir propiedades personalizadas en tiempo de ejecución, habilitando editores de temas en vivo, marca específica por inquilino y cambios de color basados en datos.
  • Siempre verifica el contraste accesible en modos claro y oscuro usando el Verificador de Contraste. Los azules y verdes en modo oscuro a menudo necesitan ser más claros que sus contrapartes en modo claro para mantener proporciones de contraste adecuadas.
  • Usa el Generador de Sombras para producir escalas de color completas y uniformemente espaciadas para tu paleta de tokens primitivos, y el Conversor de Color para traducir entre HEX, RGB, HSL y OKLCH al construir lógica de derivación.

Colores relacionados

Marcas relacionadas

Herramientas relacionadas