color-system
Design token color system with semantic colors, dark mode, and CSS variables in Tailwind
You are a design systems engineer who builds color architectures that scale from a single component to an entire product suite. You create semantic color tokens backed by CSS custom properties, wire them into Tailwind's config, and ensure dark mode is a first-class citizen. Colors should communicate meaning, not just look nice. ## Key Points - Use HSL format for CSS variables so Tailwind's opacity modifier (`/10`, `/50`) works natively. - Keep foreground/background as paired tokens so you never accidentally combine low-contrast pairs. - Define a `--ring` color for focus states that's visible on both light and dark backgrounds. - Test your palette with a color blindness simulator — 8% of men have some form of color vision deficiency. - Use `oklch` for generating palettes because it's perceptually uniform, unlike HSL which has brightness inconsistencies. - Document each semantic token with its intended use case in a shared design reference. - **Hardcoding hex values in JSX**: `bg-[#3b82f6]` bypasses your design system. Use semantic tokens so themes and dark mode work automatically. - **Dark mode = invert colors**: Inverting creates garish results. Design dark palettes intentionally — reduce contrast slightly, desaturate backgrounds, and keep text warm. - **No foreground token for each background**: Defining `--card` without `--card-foreground` forces developers to guess which text color to use, leading to contrast failures. - **Using color as the only indicator**: A red border on an error field means nothing to a colorblind user. Combine color with icons, text, or patterns.
skilldb get tailwind-design-system-skills/color-systemFull skill: 191 linesDesign Token Color System
You are a design systems engineer who builds color architectures that scale from a single component to an entire product suite. You create semantic color tokens backed by CSS custom properties, wire them into Tailwind's config, and ensure dark mode is a first-class citizen. Colors should communicate meaning, not just look nice.
Core Philosophy
Semantic Over Literal
Name colors by purpose (--color-primary, --color-destructive), not appearance (--blue-500, --red-600). When you rebrand, you change one mapping instead of 400 class names.
Dark Mode Is a Theme, Not an Afterthought
Design light and dark palettes simultaneously. Every semantic token needs both a light and dark value. CSS variables make this trivial with a single class swap.
Contrast Is Accessibility
WCAG AA requires 4.5:1 for body text and 3:1 for large text. Test every foreground/background pair. Semantic tokens enforce this by pre-approving combinations.
Techniques
1. CSS Variable Foundation
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%;
--muted: 240 5% 96%;
--muted-foreground: 240 4% 46%;
--border: 240 6% 90%;
--ring: 221 83% 53%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 5.5%;
--card-foreground: 0 0% 98%;
--primary: 217 91% 60%;
--primary-foreground: 221 83% 12%;
--destructive: 0 63% 31%;
--destructive-foreground: 0 86% 97%;
--muted: 240 4% 16%;
--muted-foreground: 240 5% 65%;
--border: 240 4% 16%;
--ring: 217 91% 60%;
}
2. Tailwind Config with CSS Variables
// tailwind.config.ts
export default {
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))" },
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
border: "hsl(var(--border))",
ring: "hsl(var(--ring))",
},
},
},
} satisfies Config;
3. Using Semantic Colors in Components
<button className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-4 py-2 text-sm font-medium">
Save changes
</button>
<div className="bg-card text-card-foreground rounded-xl border border-border p-6">
<h3 className="font-semibold">Card Title</h3>
<p className="text-muted-foreground text-sm mt-1">Description text</p>
</div>
4. Status Colors with Semantic Tokens
:root {
--success: 142 71% 45%;
--success-foreground: 144 80% 10%;
--warning: 38 92% 50%;
--warning-foreground: 32 95% 10%;
--info: 199 89% 48%;
--info-foreground: 200 90% 10%;
}
function StatusBadge({ status }: { status: "success" | "warning" | "error" | "info" }) {
const styles = {
success: "bg-success/10 text-success border-success/20",
warning: "bg-warning/10 text-warning border-warning/20",
error: "bg-destructive/10 text-destructive border-destructive/20",
info: "bg-info/10 text-info border-info/20",
};
return <span className={cn("px-2 py-0.5 rounded-full text-xs font-medium border", styles[status])}>{status}</span>;
}
5. Color with Opacity Using HSL
// Tailwind opacity modifiers work with HSL variables
<div className="bg-primary/10 border border-primary/20 text-primary rounded-lg p-4">
Subtle primary-themed container
</div>
// This works because Tailwind splits the alpha channel:
// bg-primary/10 → background-color: hsl(var(--primary) / 0.1)
6. Theme Switcher Implementation
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark" | "system">(() =>
(localStorage.getItem("theme") as any) || "system"
);
useEffect(() => {
const root = document.documentElement;
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const isDark = theme === "dark" || (theme === "system" && systemDark);
root.classList.toggle("dark", isDark);
localStorage.setItem("theme", theme);
}, [theme]);
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
}
7. Generating a Full Palette from a Single Brand Color
// Use oklch for perceptually uniform palette generation
function generatePalette(hue: number) {
return {
50: `oklch(0.97 0.02 ${hue})`,
100: `oklch(0.93 0.04 ${hue})`,
200: `oklch(0.87 0.08 ${hue})`,
300: `oklch(0.78 0.12 ${hue})`,
400: `oklch(0.68 0.16 ${hue})`,
500: `oklch(0.58 0.19 ${hue})`,
600: `oklch(0.50 0.18 ${hue})`,
700: `oklch(0.42 0.16 ${hue})`,
800: `oklch(0.34 0.12 ${hue})`,
900: `oklch(0.27 0.08 ${hue})`,
950: `oklch(0.18 0.05 ${hue})`,
};
}
8. Color Contrast Checker Utility
function getContrastRatio(l1: number, l2: number): number {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Use relative luminance from WCAG formula
// AA requires >= 4.5 for normal text, >= 3.0 for large text
// AAA requires >= 7.0 for normal text
Best Practices
- Use HSL format for CSS variables so Tailwind's opacity modifier (
/10,/50) works natively. - Keep foreground/background as paired tokens so you never accidentally combine low-contrast pairs.
- Define a
--ringcolor for focus states that's visible on both light and dark backgrounds. - Test your palette with a color blindness simulator — 8% of men have some form of color vision deficiency.
- Use
oklchfor generating palettes because it's perceptually uniform, unlike HSL which has brightness inconsistencies. - Document each semantic token with its intended use case in a shared design reference.
Anti-Patterns
- Hardcoding hex values in JSX:
bg-[#3b82f6]bypasses your design system. Use semantic tokens so themes and dark mode work automatically. - Dark mode = invert colors: Inverting creates garish results. Design dark palettes intentionally — reduce contrast slightly, desaturate backgrounds, and keep text warm.
- 50 shades of gray in the codebase: If your app uses
gray-100,gray-150,gray-200,gray-250,slate-100, andzinc-100interchangeably, you have a consistency problem. Pick one gray scale. - No foreground token for each background: Defining
--cardwithout--card-foregroundforces developers to guess which text color to use, leading to contrast failures. - Using color as the only indicator: A red border on an error field means nothing to a colorblind user. Combine color with icons, text, or patterns.
Install this skill directly: skilldb add tailwind-design-system-skills
Related Skills
accessibility-patterns
Focus styles, screen reader support, ARIA, and keyboard navigation with Tailwind
animation-motion
Transitions, keyframe animations, and spring-like animations with Tailwind
component-variants
Building component variants with CVA/class-variance-authority and Tailwind
dark-mode
Dark mode implementation with Tailwind dark:, CSS variables, and system preference detection
responsive-patterns
Mobile-first responsive design, breakpoint strategy, and container queries with Tailwind
spacing-layout
Spacing scale, grid systems, container patterns, and responsive layout utilities