Volver a Sistemas de Diseño
Tema 6 de 8
Tematización y Personalización
Implementa modo oscuro, tematización de marca e interfaces personalizables por el usuario
Por Qué Importa la Tematización
Las aplicaciones modernas necesitan soportar modo oscuro, personalización de marca y preferencias de usuario. Un buen sistema de tematización te permite cambiar toda la apariencia de tu aplicación modificando unas pocas variables, mientras mantienes tu código de componentes limpio.
🎨 Estrategias de Tematización
Variables CSS Personalizadas
Variables CSS nativas, funciona en todas partes
Temas CSS-in-JS
Objetos de tema en styled-components, Emotion
Tailwind + Variables CSS
Lo mejor de ambos mundos
React Context
Patrón de proveedor de temas
Variables CSS Personalizadas para Tematización
/* tokens.css */
:root {
/* Tema claro (predeterminado) */
--background: #ffffff;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--primary: #3b82f6;
--primary-foreground: #ffffff;
--secondary: #f1f5f9;
--secondary-foreground: #1e293b;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--border: #e2e8f0;
--ring: #3b82f6;
}
/* Tema oscuro */
.dark {
--background: #0a0a0a;
--foreground: #fafafa;
--card: #1c1c1c;
--card-foreground: #fafafa;
--primary: #60a5fa;
--primary-foreground: #0a0a0a;
--secondary: #27272a;
--secondary-foreground: #fafafa;
--muted: #27272a;
--muted-foreground: #a1a1aa;
--border: #27272a;
--ring: #60a5fa;
}
/* Usando los tokens */
.card {
background-color: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
}
.button-primary {
background-color: var(--primary);
color: var(--primary-foreground);
}
Alternador de Modo Oscuro en React
// hooks/useTheme.ts
import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
export function useTheme() {
const [theme, setTheme] = useState(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as Theme) || 'system';
});
useEffect(() => {
const root = document.documentElement;
const applyTheme = (t: Theme) => {
if (t === 'system') {
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.classList.toggle('dark', systemDark);
} else {
root.classList.toggle('dark', t === 'dark');
}
};
applyTheme(theme);
localStorage.setItem('theme', theme);
// Escuchar cambios de preferencia del sistema
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => theme === 'system' && applyTheme('system');
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [theme]);
return { theme, setTheme };
}
// Componente ThemeToggle
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
);
}
next-themes (Next.js)
Para aplicaciones Next.js, next-themes maneja SSR, preferencia del sistema y prevención de flash:
npm install next-themes
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
{children}
);
}
// ThemeToggle.tsx
'use client';
import { useTheme } from 'next-themes';
import { Sun, Moon, Monitor } from 'lucide-react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
);
}
Tematización de Marca / Multi-tenencia
// themes/brands.ts
export const brands = {
default: {
primary: '#3b82f6',
primaryForeground: '#ffffff',
accent: '#f59e0b',
},
acme: {
primary: '#10b981',
primaryForeground: '#ffffff',
accent: '#8b5cf6',
},
corp: {
primary: '#ef4444',
primaryForeground: '#ffffff',
accent: '#06b6d4',
},
};
// Aplicar tema de marca
function applyBrandTheme(brand: keyof typeof brands) {
const theme = brands[brand];
const root = document.documentElement;
Object.entries(theme).forEach(([key, value]) => {
// Convertir camelCase a kebab-case para CSS
const cssVar = key.replace(/([A-Z])/g, '-$1').toLowerCase();
root.style.setProperty(`--${cssVar}`, value);
});
}
// Uso: applyBrandTheme('acme');
// O en configuración de Tailwind
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: 'var(--primary)',
'primary-foreground': 'var(--primary-foreground)',
accent: 'var(--accent)',
},
},
},
};
Tailwind + Variables CSS (enfoque shadcn/ui)
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
}
}
// tailwind.config.js
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
};
// Ahora usa clases de Tailwind que referencian variables CSS
Utilidades de Esquema de Color
// Genera paletas de colores consistentes
// Usa herramientas como https://www.tailwindshades.com/
// O programáticamente con manipulación de color
import { colord, extend } from 'colord';
import a11yPlugin from 'colord/plugins/a11y';
extend([a11yPlugin]);
function generatePalette(baseColor: string) {
const color = colord(baseColor);
return {
50: color.lighten(0.45).toHex(),
100: color.lighten(0.35).toHex(),
200: color.lighten(0.25).toHex(),
300: color.lighten(0.15).toHex(),
400: color.lighten(0.05).toHex(),
500: color.toHex(),
600: color.darken(0.1).toHex(),
700: color.darken(0.2).toHex(),
800: color.darken(0.3).toHex(),
900: color.darken(0.4).toHex(),
};
}
// Verificar contraste para accesibilidad
function getContrastColor(bgColor: string) {
const color = colord(bgColor);
return color.isLight() ? '#000000' : '#ffffff';
}
// Asegurar cumplimiento WCAG AA
function checkContrast(foreground: string, background: string) {
const ratio = colord(foreground).contrast(background);
return {
ratio,
passesAA: ratio >= 4.5,
passesAAA: ratio >= 7,
};
}
💡 Mejores Prácticas
- • Usa nombres de tokens semánticos:
--primaryno--blue - • Siempre define valores de tema claro y oscuro
- • Usa la media query
prefers-color-schemepara detección del sistema - • Persiste la preferencia de tema en localStorage
- • Previene el flash de tema incorrecto al cargar la página (usa next-themes o script bloqueante)
- • Prueba el contraste de color para accesibilidad (mínimo 4.5:1)