Skip to main content
Visual Arts & DesignTailwind Design System191 lines

color-system

Design token color system with semantic colors, dark mode, and CSS variables in Tailwind

Quick Summary16 lines
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 lines
Paste into your CLAUDE.md or agent config

Design 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 --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.

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, and zinc-100 interchangeably, you have a consistency problem. Pick one gray scale.
  • 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.

Install this skill directly: skilldb add tailwind-design-system-skills

Get CLI access →