Temas de Color en React: Variables CSS y Context
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
La temática es uno de esos problemas que parece simple hasta que tienes que mantenerlo a escala. Un simple toggle entre modo claro y oscuro es directo. Un producto que sirve a múltiples marcas, ofrece paletas personalizables por el usuario o necesita cambiar de tema al instante sin recargar la página requiere una arquitectura más deliberada.
El modelo de componentes de React y las propiedades CSS personalizadas son un emparejamiento natural para la temática. Las variables CSS manejan los valores de color de forma declarativa; React Context gestiona el estado del tema; y Tailwind CSS (en proyectos modernos) hace de puente entre ambos. Esta guía cubre cada capa: cómo estructurar la arquitectura de temas, implementar la base de variables CSS, conectar el estado de React y manejar escenarios multimarca.
Patrones de Arquitectura de Temas
Patrón 1: Solo Variables CSS (Sin Estado JavaScript)
El patrón más simple para soporte básico de modo oscuro no requiere ningún estado de React. Una media query prefers-color-scheme cambia los valores de las propiedades personalizadas y cada componente se actualiza automáticamente:
/* globals.css */
:root {
--color-bg: #F8FAFC;
--color-text: #1E293B;
--color-brand: #2563EB;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0F172A;
--color-text: #F1F5F9;
--color-brand: #60A5FA;
}
}
Los componentes usan var(--color-bg) y nunca necesitan saber sobre el tema actual. El navegador lo maneja todo.
Cuándo usar: Sitios donde quieres respetar la preferencia a nivel del sistema operativo y no necesitas un toggle controlado por el usuario.
Limitación: No hay forma de que los usuarios sobreescriban su configuración del sistema operativo dentro de la aplicación.
Patrón 2: Cambio de Tema con Atributo de Datos
Agrega un atributo data-theme en el elemento <html> para hacer el tema activo explícito y sobrescribible. Esto es compatible con SSR, evita el destello del tema incorrecto y se combina trivialmente con persistencia en localStorage:
/* Por defecto: claro */
[data-theme="light"],
:root {
--color-bg: #F8FAFC;
--color-text: #1E293B;
--color-brand: #2563EB;
--color-surface: #FFFFFF;
--color-border: #E2E8F0;
}
/* Oscuro */
[data-theme="dark"] {
--color-bg: #0F172A;
--color-text: #F1F5F9;
--color-brand: #60A5FA;
--color-surface: #1E293B;
--color-border: #334155;
}
// Aplicar el tema antes del primer renderizado (script en línea en <head>)
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
Cuándo usar: La mayoría de las aplicaciones de producción que necesitan un toggle controlado por el usuario. El enfoque de atributo de datos es el estándar actual de la industria, utilizado por la implementación de modo oscuro de Tailwind CSS, Radix UI y la mayoría de los design systems modernos.
Patrón 3: Cambio de Tema Basado en Clases
Similar al atributo de datos, pero usa clases CSS. La configuración darkMode: 'class' de Tailwind depende de este patrón:
.dark {
--color-bg: #0F172A;
--color-text: #F1F5F9;
}
Agregar la clase dark a <html> activa los valores del token oscuro. Tailwind luego aplica las utilidades con prefijo dark: cuando esta clase está presente.
Cuándo usar: Proyectos que usan Tailwind CSS con la estrategia de modo oscuro class.
Variables CSS para Colores: El Sistema de Tokens
Nomenclatura Semántica vs. Nomenclatura Descriptiva
La decisión más importante en un sistema de tokens es la nomenclatura. Nombrar por valor de color (--blue-500, --gray-900) hace que la variable sea fácil de entender de forma aislada pero imposible de tematizar: cambiar --blue-500 a púrpura rompe la semántica del nombre.
Nombrar por rol semántico (--color-brand, --color-text-muted) permite que los valores cambien completamente entre temas mientras los componentes permanecen correctos:
:root {
/* ---- Primitivas de Color (no se usan directamente en componentes) ---- */
--blue-500: #3B82F6;
--blue-700: #1D4ED8;
--slate-50: #F8FAFC;
--slate-800: #1E293B;
--slate-900: #0F172A;
/* ---- Tokens Semánticos (usados en componentes) ---- */
/* Texto */
--text-primary: var(--slate-800);
--text-secondary: #64748B;
--text-disabled: #94A3B8;
--text-inverse: var(--slate-50);
/* Superficies */
--bg-base: var(--slate-50);
--bg-elevated: #FFFFFF;
--bg-sunken: #F1F5F9;
/* Marca */
--brand: var(--blue-700);
--brand-hover: #1E40AF;
--brand-subtle: #EFF6FF;
--on-brand: #FFFFFF;
/* Retroalimentación */
--error: #DC2626;
--error-bg: #FEF2F2;
--success: #16A34A;
--success-bg: #F0FDF4;
--warning: #D97706;
--warning-bg: #FFFBEB;
}
[data-theme="dark"] {
--text-primary: #F1F5F9;
--text-secondary: #94A3B8;
--text-disabled: #475569;
--text-inverse: #0F172A;
--bg-base: #0F172A;
--bg-elevated: #1E293B;
--bg-sunken: #0D1426;
--brand: #60A5FA;
--brand-hover: #93C5FD;
--brand-subtle: #172554;
--on-brand: #0F172A;
--error: #F87171;
--error-bg: #1C0A0A;
--success: #4ADE80;
--success-bg: #052E16;
--warning: #FCD34D;
--warning-bg: #1C1000;
}
El sistema de dos capas (primitivas + tokens semánticos) te da lo mejor de ambos mundos: una paleta de colores sin procesar como referencia y tokens semánticos para el uso en componentes.
Generando tu Escala de Colores
Antes de construir el sistema de tokens, necesitas la paleta de colores sin procesar. El Generador de Tonos produce una escala completa del 50 al 950 desde un único color de marca, el mismo patrón de escala que usa Tailwind CSS. Ingresa tu código hexadecimal de marca y obtén el conjunto completo de variantes de oscuro a claro listas para usar como tokens primitivos.
Por ejemplo, al ingresar #2563EB como tu azul de marca se genera:
blue-50: #EFF6FFblue-100: #DBEAFEblue-500: #3B82F6blue-700: #1D4ED8blue-900: #1E3A8Ablue-950: #172554
Estos valores primitivos luego pueblan tus tokens semánticos, asegurando que los colores del modo oscuro estén armónicamente relacionados con los colores del modo claro en lugar de ser aproximaciones oscuras arbitrarias.
React Context para el Estado del Tema
El Patrón ThemeContext
Para aplicaciones con un toggle de tema controlado por el usuario, React Context proporciona la capa de gestión de estado. El contexto almacena el nombre del tema activo y expone una función de toggle:
// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextValue {
theme: Theme;
resolvedTheme: 'light' | 'dark'; // Tema realmente aplicado (resuelve 'system')
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as Theme) || 'system';
});
const systemPrefersDark = typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false;
const resolvedTheme: 'light' | 'dark' =
theme === 'system'
? (systemPrefersDark ? 'dark' : 'light')
: theme;
useEffect(() => {
const root = document.documentElement;
root.setAttribute('data-theme', resolvedTheme);
localStorage.setItem('theme', theme);
}, [theme, resolvedTheme]);
// Escuchar cambios de preferencia del sistema cuando theme === 'system'
useEffect(() => {
if (theme !== 'system') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => {
document.documentElement.setAttribute(
'data-theme',
media.matches ? 'dark' : 'light'
);
};
media.addEventListener('change', handler);
return () => media.removeEventListener('change', handler);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme debe usarse dentro de ThemeProvider');
return ctx;
}
El ThemeProvider envuelve tu aplicación (o el subárbol relevante) y el hook useTheme expone el estado del tema en cualquier lugar del árbol de componentes:
// components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
aria-label={`Cambiar a modo ${theme === 'dark' ? 'claro' : 'oscuro'}`}
style={{
background: 'var(--bg-elevated)',
color: 'var(--text-primary)',
border: '1px solid var(--color-border)',
padding: '8px 16px',
borderRadius: '8px',
cursor: 'pointer',
}}
>
{theme === 'dark' ? '☀ Claro' : '☾ Oscuro'}
</button>
);
}
Los componentes usan propiedades CSS personalizadas para sus estilos visuales: no necesitan leer resolvedTheme para aplicar colores. El contexto solo se necesita para la interfaz que muestra o controla el estado del tema actual.
Evitar el Destello del Tema Incorrecto (FOTWT)
Las aplicaciones renderizadas en el servidor enfrentan el problema del destello del tema incorrecto: el servidor renderiza HTML sin conocer la preferencia de tema del usuario, el navegador muestra ese HTML momentáneamente, luego React se hidrata y aplica el tema correcto, causando un destello visible.
La solución es un script en línea bloqueante en <head> que lee localStorage y aplica el atributo de tema antes de que la página se renderice:
<!-- En tu <head>, antes de los enlaces CSS -->
<script>
(function() {
try {
var stored = localStorage.getItem('theme');
var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = stored === 'dark' || stored === 'light'
? stored
: (systemDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
} catch(e) {}
})();
</script>
En Next.js, esto va en un _document.tsx o en el layout.tsx raíz del App Router. Dado que este script se ejecuta síncronamente antes de que se analice cualquier CSS, no hay destello.
Temas con Tailwind CSS
Tailwind v3: Modo Oscuro Basado en Clases
En Tailwind v3, el modo oscuro requiere establecer darkMode: 'class' en tailwind.config.js. Esto hace que Tailwind aplique las utilidades dark: solo cuando la clase .dark está en el elemento <html>:
// tailwind.config.js
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#EFF6FF',
100: '#DBEAFE',
500: '#3B82F6',
600: '#2563EB',
700: '#1D4ED8',
900: '#1E3A8A',
950: '#172554',
},
},
},
},
};
Los componentes usan prefijos dark: para variantes de modo oscuro:
<div className="bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100">
<button className="bg-brand-600 dark:bg-brand-500 text-white hover:bg-brand-700 dark:hover:bg-brand-400">
Acción Principal
</button>
</div>
El toggle de tema establece la clase .dark en document.documentElement, el mismo enfoque que el patrón de atributo de datos, solo usando una clase en lugar de un atributo.
Tailwind v4: Configuración CSS-First
Tailwind v4 mueve la configuración completamente a CSS, usando propiedades CSS personalizadas de forma nativa:
/* styles.css */
@import "tailwindcss";
@theme {
--color-brand-50: #EFF6FF;
--color-brand-100: #DBEAFE;
--color-brand-500: #3B82F6;
--color-brand-600: #2563EB;
--color-brand-700: #1D4ED8;
}
Tailwind v4 genera clases de utilidad a partir de estas propiedades personalizadas, que son inmediatamente utilizables en JSX: className="bg-brand-600 text-white". La configuración del modo oscuro en v4 usa @variant dark:
@variant dark (&:where([data-theme="dark"] *)) {
/* Tailwind aplica las utilidades dark: basándose en este selector */
}
Esta variante le indica a Tailwind que aplique las utilidades dark: cuando un ancestro tiene data-theme="dark", integrándose nativamente con el patrón de atributo de datos.
Estrategia de Temas Multimarca
El Desafío
Una plataforma SaaS que sirve a múltiples clientes, un producto de marca blanca o un design system compartido entre múltiples marcas de productos necesita manejar no solo variantes oscuras/claras sino identidades de color completamente diferentes, cada una con su propio color primario de marca, acento, colores de estado y neutros.
Tokens de Marca como Sobreescrituras de Variables CSS
El enfoque más mantenible define tokens semánticos independientes de la marca en una hoja de estilos base y luego sobreescribe las asignaciones de primitivas por marca:
/* Tema base — los mismos nombres de token para todas las marcas */
:root {
/* Primitivas de Marca — sobreescritas por marca */
--brand-primary-raw: 37 99 235; /* #2563EB en canales RGB */
--brand-accent-raw: 99 102 241; /* #6366F1 en canales RGB */
/* Tokens semánticos — calculados desde primitivas */
--brand-primary: rgb(var(--brand-primary-raw));
--brand-primary-hover: color-mix(in srgb, rgb(var(--brand-primary-raw)) 80%, black);
--brand-primary-subtle: color-mix(in srgb, rgb(var(--brand-primary-raw)) 10%, white);
}
/* Marca: Acme Corp (azul) */
[data-brand="acme"] {
--brand-primary-raw: 37 99 235; /* #2563EB */
--brand-accent-raw: 16 185 129; /* #10B981 */
}
/* Marca: Globex Inc (púrpura) */
[data-brand="globex"] {
--brand-primary-raw: 124 58 237; /* #7C3AED */
--brand-accent-raw: 245 158 11; /* #F59E0B */
}
/* Marca: Initech (verde) */
[data-brand="initech"] {
--brand-primary-raw: 22 163 74; /* #16A34A */
--brand-accent-raw: 239 68 68; /* #EF4444 */
}
El uso de la técnica de canales RGB sin procesar (37 99 235) permite variantes de opacidad sin tokens adicionales:
.overlay {
background-color: rgba(var(--brand-primary-raw), 0.1);
}
React Context Multimarca
Extiende el contexto del tema para manejar la marca:
// contexts/ThemeContext.tsx
type Brand = 'acme' | 'globex' | 'initech' | 'default';
type Theme = 'light' | 'dark';
interface ThemeContextValue {
theme: Theme;
brand: Brand;
setTheme: (theme: Theme) => void;
setBrand: (brand: Brand) => void;
}
export function ThemeProvider({ initialBrand = 'default', children }: {
initialBrand?: Brand;
children: React.ReactNode;
}) {
const [theme, setTheme] = useState<Theme>('light');
const [brand, setBrand] = useState<Brand>(initialBrand);
useEffect(() => {
const root = document.documentElement;
root.setAttribute('data-theme', theme);
root.setAttribute('data-brand', brand);
}, [theme, brand]);
return (
<ThemeContext.Provider value={{ theme, brand, setTheme, setBrand }}>
{children}
</ThemeContext.Provider>
);
}
Establece la marca inicial desde tu capa de enrutamiento o autenticación: el subdominio o ID de inquilino de cada cliente se mapea a un identificador de marca.
Generando Escalas de Marca con el Generador de Tonos
Para cada marca, necesitas una escala de colores completa, no solo uno o dos valores hexadecimales. Usa el Generador de Tonos para generar una escala del 50 al 950 para el color primario y de acento de cada marca. Ingresa el hexadecimal primario de la marca y obtén el rango completo de variantes de oscuro a claro.
Para Globex Inc con el primario #7C3AED, el generador de tonos produce la escala de púrpura completa. Para Initech con #16A34A, la escala de verde completa. Estas escalas luego pueblan los tokens primitivos para cada marca, asegurando que --brand-primary-subtle (el tinte más claro) y --brand-primary-hover (un estado presionado más oscuro) permanezcan armónicamente consistentes dentro de cada identidad de marca.
Conclusiones Clave
- Las propiedades CSS personalizadas son la base: Define tokens semánticos (
--text-primary,--brand-primary) y cambia sus valores por tema, en lugar de aplicar condicionalmente clases en cada componente. - El cambio de tema con atributo de datos (
data-theme="dark") es el patrón más flexible: es sobrescribible por JavaScript, compatible con SSR y combinable con media queriesprefers-color-scheme. - El sistema de dos capas de tokens (escala de colores primitivos + tokens de rol semántico) permite total flexibilidad de tema manteniendo los componentes limpios: las primitivas definen tu paleta, los semánticos definen cómo se usan los colores.
- Usa el Generador de Tonos para generar una escala de colores completa del 50 al 950 desde tu marca principal, lo que te da el rango completo de variantes claras y oscuras necesario para un sistema de tokens exhaustivo.
- React Context gestiona el estado del tema (el nombre del tema activo) y los efectos secundarios (establecer el atributo de datos, persistir en localStorage); los componentes nunca necesitan leer el contexto para aplicar los colores correctos.
- Evita el destello del tema incorrecto con un script en línea bloqueante en
<head>que leelocalStoragey aplica el atributo de tema antes de que el navegador renderice cualquier píxel. - Integración con Tailwind CSS: v3 usa
darkMode: 'class'con prefijosdark:; v4 usa configuración CSS-first@themecon soporte nativo de variables CSS. - La temática multimarca apila otro atributo de datos (
data-brand) sobre el sistema de temas, sobreescribiendo los valores de tokens primitivos por marca mientras mantiene sin cambios los tokens semánticos y los componentes.