Personalización de Colores en shadcn/ui: Tematización con Variables CSS
shadcn/ui se ha convertido en la biblioteca de componentes predeterminada para toda una generación de desarrolladores de React. A diferencia de las bibliotecas tradicionales que distribuyen un paquete precompilado, los componentes de shadcn/ui se copian directamente en tu proyecto —lo que significa que eres dueño del código, incluidas todas las decisiones de color. La biblioteca incluye un sistema de variables CSS basado en HSL bien pensado que hace que la tematización sea sencilla, pero su máximo potencial solo se desbloquea cuando entiendes la arquitectura que hay detrás. Esta guía recorre el sistema de colores de principio a fin, pasando por la creación de temas personalizados, la integración del modo oscuro y la sincronización con tu configuración de Tailwind CSS.
Descripción General de la Arquitectura de Colores de shadcn/ui
Cuando inicializas un proyecto de shadcn/ui (npx shadcn@latest init), el CLI añade un bloque de propiedades personalizadas CSS a tu hoja de estilos global:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... sobreescrituras del modo oscuro ... */
}
}
Dos decisiones estructurales condicionan todo lo demás:
-
Nombramiento semántico: Las variables se nombran según sus roles (
--primary,--destructive,--muted) en lugar de valores visuales (--blue-600,--red-500). Un componente que usa--primaryno sabe ni le importa si primary es azul, violeta o naranja. -
Valores HSL solo de canales: Las variables almacenan solo los canales HSL —
222.2 84% 4.9%— no una llamada completa a la funciónhsl(). Esto es intencional: los componentes consumen la variable comohsl(var(--primary)), lo que permite que el modificador de opacidad se componga de forma natural:hsl(var(--primary) / 0.5).
El Sistema de Variables CSS en HSL
Por Qué Solo los Canales HSL
Almacenar el color como 222.2 84% 4.9% (solo canales) en lugar de hsl(222.2, 84%, 4.9%) (un valor completo) habilita un patrón que las variables CSS estándar no soportan de forma elegante:
/* CSS estándar — la composición alfa requiere una variable separada */
:root {
--primary: hsl(222.2, 84%, 11.2%);
--primary-alpha-50: hsl(222.2, 84%, 11.2%, 0.5); /* hay que duplicar */
}
/* Patrón shadcn/ui — el alfa se compone sin duplicación */
:root {
--primary: 222.2 47.4% 11.2%;
}
.element {
background: hsl(var(--primary)); /* opaco */
border-color: hsl(var(--primary) / 0.2); /* 20% de opacidad */
}
El valor de solo canales es un fragmento que solo tiene sentido dentro de hsl(). Esto limita dónde puede usarse la variable, pero esa limitación es la característica: impone una composición de opacidad consistente en todo tu código base.
El Mapa Completo de Variables
shadcn/ui define 16 roles de color (excluyendo --radius):
| Variable | Propósito | Consumidor típico |
|---|---|---|
--background |
Fondo de página | <body>, contenedores de página |
--foreground |
Texto del cuerpo | <p>, <span> |
--card |
Superficie de tarjeta | <Card> |
--card-foreground |
Texto en tarjetas | Contenido de tarjeta |
--popover |
Superficie flotante | <Popover>, <DropdownMenu> |
--popover-foreground |
Texto en popovers | Contenido de popover |
--primary |
Interactivo principal | <Button variant="default"> |
--primary-foreground |
Texto sobre primary | Etiqueta de botón |
--secondary |
Superficie secundaria | <Button variant="secondary"> |
--secondary-foreground |
Texto sobre secondary | Etiqueta de botón secundario |
--muted |
Fondo atenuado | <Badge>, estados deshabilitados |
--muted-foreground |
Texto atenuado | Metadatos, marcadores de posición |
--accent |
Estado hover/seleccionado | Hover de ítem en menús |
--accent-foreground |
Texto sobre accent | Etiqueta de ítem con hover |
--destructive |
Peligro/error | <Button variant="destructive"> |
--destructive-foreground |
Texto sobre destructive | Etiqueta de botón destructivo |
--border |
Borde predeterminado | Divisores, contornos de input |
--input |
Borde de campo de entrada | <Input>, <Select> |
--ring |
Anillo de enfoque | Indicador de foco de teclado |
Creación de Temas Personalizados
Enfoque 1: Usar el Constructor de Temas Oficial
El sitio web de shadcn/ui incluye un constructor de temas en ui.shadcn.com/themes. Eliges un tono de color base y una escala de grises, y genera el bloque CSS completo para los modos claro y oscuro. Este es el enfoque más rápido para cambios simples de color de marca.
Enfoque 2: Construir a Partir de un Color de Marca
Para un control completo, comienza con el color primario de tu marca y deriva manualmente el conjunto completo de variables.
Supongamos que tu primary de marca es #6D28D9 (un violeta profundo). Conviértelo a HSL usando el Convertidor de Color: aproximadamente hsl(263, 70%, 50%). Los canales HSL que necesitas son 263 70% 50%.
A partir de ahí, deriva la paleta completa:
@layer base {
:root {
/* Variables derivadas de la marca */
--background: 0 0% 100%;
--foreground: 263 20% 8%; /* casi negro con toque violeta */
--card: 0 0% 100%;
--card-foreground: 263 20% 8%;
--popover: 0 0% 100%;
--popover-foreground: 263 20% 8%;
/* Primary: tu violeta de marca */
--primary: 263 70% 50%;
--primary-foreground: 263 10% 98%; /* casi blanco */
/* Secondary: un tinte violeta claro */
--secondary: 263 30% 94%;
--secondary-foreground: 263 70% 30%;
/* Muted: secondary desaturado */
--muted: 260 20% 95%;
--muted-foreground: 260 15% 50%;
/* Accent: estado hover (ligeramente más saturado que secondary) */
--accent: 263 40% 90%;
--accent-foreground: 263 70% 30%;
/* Destructive: rojo estándar */
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%;
/* Bordes y controles */
--border: 263 20% 88%;
--input: 263 20% 88%;
--ring: 263 70% 50%; /* coincide con primary — el anillo de foco refleja la marca */
--radius: 0.5rem;
}
}
Consejos para Derivar Cada Rol
--primary-foreground: El primer plano sobre tu botón primary debe cumplir WCAG AA (4.5:1) respecto a --primary. Para colores muy saturados, el casi-blanco (luminosidad del 98%) generalmente funciona. Verifica con el Verificador de Contraste.
--secondary y --muted: Típicamente son el mismo tono que --primary pero con la saturación reducida al 20–35% y la luminosidad por encima del 90%. Crean cohesión visual sin competir con el primary.
--accent: Es el fondo hover para ítems de lista interactivos (opciones de combobox, ítems de menú desplegable). Debe ser visible pero no chocante —una luminosidad del 88–92% con saturación moderada funciona para la mayoría de tonos.
--ring: El anillo de foco de teclado debe coincidir exactamente con tu color primary, para que los usuarios de teclado vean el mismo color de marca que los usuarios de ratón ven en los fondos de botón. La accesibilidad requiere que el anillo sea visible: contraste de 3:1 respecto al fondo adyacente.
--destructive: No lo cambies para que coincida con tu marca a menos que tu marca sea roja. Los usuarios tienen una expectativa profundamente condicionada de que las acciones destructivas son rojas. Mantenlo en el rango de tono 0–10°.
Integración del Modo Oscuro
shadcn/ui usa la clase .dark en <html> (no [data-theme="dark"]) como selector de modo oscuro. Esto coincide con la configuración darkMode: 'class' de Tailwind CSS.
Definición de Valores del Modo Oscuro
El bloque del modo oscuro sigue la misma estructura pero invierte las relaciones de luminosidad:
.dark {
--background: 263 25% 7%; /* gris-violeta muy oscuro */
--foreground: 263 10% 92%;
--card: 263 25% 10%;
--card-foreground: 263 10% 92%;
--popover: 263 25% 10%;
--popover-foreground: 263 10% 92%;
/* Primary: mismo tono, tono más claro para fondo oscuro */
--primary: 263 70% 70%; /* era 50% en modo claro, ahora 70% de luminosidad */
--primary-foreground: 263 30% 8%; /* texto oscuro sobre botón claro */
--secondary: 263 25% 18%;
--secondary-foreground: 263 70% 85%;
--muted: 263 20% 15%;
--muted-foreground: 263 15% 55%;
--accent: 263 30% 22%;
--accent-foreground: 263 70% 85%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 98%;
--border: 263 20% 20%;
--input: 263 20% 20%;
--ring: 263 70% 70%; /* coincide con primary */
}
El ajuste clave es --primary: en modo claro tenía luminosidad del 50% (saturado, contrasta sobre blanco). En modo oscuro se convierte en luminosidad del 70% —más claro y aún saturado, que contrasta con el --background casi negro. Un color primary que supera el contraste en modo claro casi con seguridad fallará en modo oscuro sin este ajuste.
Usa el Generador de Tonos para explorar rápidamente tu tono en todos los niveles de luminosidad. Introduce #6D28D9 y examina el rango 200–400 para tonos primary adecuados para el modo oscuro.
Integración con el Toggle de JavaScript
El modo oscuro de shadcn/ui funciona con cualquier mecanismo que añada/elimine la clase .dark en <html>. La integración más habitual en proyectos Next.js usa next-themes:
npm install next-themes
// app/providers.tsx
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class" // añade la clase 'dark' a <html>
defaultTheme="system" // respetar la preferencia del SO
enableSystem // permitir detección de preferencia del sistema
>
{children}
</ThemeProvider>
)
}
// components/theme-toggle.tsx
import { useTheme } from 'next-themes'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Cambiar tema
</button>
)
}
Para proyectos Vite/React puro sin next-themes, gestiona la clase directamente con localStorage:
// utils/theme.ts
export function getTheme(): 'light' | 'dark' {
const stored = localStorage.getItem('theme') as 'light' | 'dark' | null
if (stored) return stored
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
export function applyTheme(theme: 'light' | 'dark') {
document.documentElement.classList.toggle('dark', theme === 'dark')
localStorage.setItem('theme', theme)
}
Llama a applyTheme(getTheme()) en <head> antes del primer renderizado para evitar el parpadeo.
Sincronizar Temas de shadcn con la Configuración de Tailwind
Los componentes de shadcn/ui usan clases de utilidad de Tailwind que referencian las variables CSS —bg-background, text-foreground, border-border, y así sucesivamente. Estas utilidades solo existen porque shadcn/ui las configura en tailwind.config.js o tailwind.config.ts:
// tailwind.config.ts — generado por shadcn init
import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: ['class'],
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
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))',
},
// ... etc
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
},
},
},
}
Esta configuración vincula el sistema de clases de utilidad de Tailwind a las variables CSS. bg-primary genera background-color: hsl(var(--primary)), que a su vez lee los canales HSL establecidos para --primary en CSS. El color completo no está codificado directamente en el CSS generado por Tailwind —se resuelve en tiempo de ejecución a partir de la variable.
Añadir Colores de Escala de Marca Junto a los Tokens Semánticos
Un requisito habitual es usar la escala completa de tonos de tu marca en componentes (p.ej., text-brand-700) mientras también se tienen las utilidades semánticas bg-primary. Estos coexisten sin conflicto:
// tailwind.config.ts
const config: Config = {
theme: {
extend: {
colors: {
// Tokens semánticos (shadcn/ui)
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
// Tokens de escala (marca)
brand: {
50: '#F5F3FF',
100: '#EDE9FE',
200: '#DDD6FE',
300: '#C4B5FD',
400: '#A78BFA',
500: '#8B5CF6',
600: '#7C3AED',
700: '#6D28D9',
800: '#5B21B6',
900: '#4C1D95',
950: '#2E1065',
},
},
},
},
}
Con esta configuración, obtienes tanto bg-primary (dinámico, consciente del tema) como bg-brand-700 (#6D28D9, estático). Usa el token semántico para los valores predeterminados de los componentes y el token de escala para sobreescrituras puntuales donde necesites un tono específico independientemente del tema actual.
Compatibilidad con Tailwind v4
Si adoptas Tailwind v4 con shadcn/ui, la configuración se mueve a @theme en CSS:
@import "tailwindcss";
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
/* ... etc */
}
Nótese la doble indirección de variables: --color-primary (convención de nomenclatura de propiedades personalizadas CSS de Tailwind) referencia hsl(var(--primary)) (la variable de shadcn/ui). Esto es ligeramente incómodo pero completamente funcional y necesario para conectar ambos sistemas en v4.
Conclusiones Clave
- El sistema de colores de shadcn/ui usa variables CSS de solo canales HSL (p.ej.,
222.2 47.4% 11.2%) en lugar de valoreshsl()completos. Esto permite opacidad composable:hsl(var(--primary) / 0.5). - Los 16 roles de color semántico (
--background,--primary,--destructive, etc.) desacoplan el marcado de los componentes de valores de color específicos —tematizas el sistema cambiando los valores de las variables, no el código de los componentes. - Para temas personalizados, comienza con el hex primary de tu marca, conviértelo a HSL con el Convertidor de Color y luego deriva los roles secondary y muted reduciendo la saturación y ajustando la luminosidad.
- El modo oscuro requiere atención específica a
--primary: un tono con luminosidad del 50% que funciona sobre fondos blancos necesita moverse al 65–75% de luminosidad para mantener el contraste sobre fondos oscuros. - La configuración de Tailwind mapea las clases de utilidad (
bg-primary,text-foreground) a consultas de variables CSS (hsl(var(--primary))), vinculando ambos sistemas. Los colores basados en escala pueden coexistir con los tokens semánticos en la misma configuración. - Usa el Generador de Tonos para producir una escala completa de marca para casos de uso con tonos exactos, y el Generador de Paletas para explorar opciones de color secundario complementarias.
Colores relacionados
Marcas relacionadas
Herramientas relacionadas
Generador de tonos
Genera escalas de tonos estilo Tailwind CSS (50–950) a partir de cualquier color base para sistemas de diseño.
Generador de paletas
Genera paletas de colores armoniosas usando esquemas complementarios, análogos, triádicos y complementarios divididos.