Imagine this: You're six months into building your design system. Your color palette has grown from 10 colors to 150+ tokens. Your developer asks, "Should I use blue-400 or primary-light for the hover state?" Your designer says, "Actually, we need a new color for the dark mode warning state." Someone else suggests warning-dark-alt.

Chaos ensues.

This is the color naming problem that plagues growing design teams. Poor color naming creates confusion, inconsistency, and technical debt that compounds over time. Good color naming, on the other hand, scales effortlessly and makes your design system a joy to use.

In this guide, we'll cover the complete framework for naming colors in design systems, including:

  • Why semantic naming beats descriptive naming
  • The three-layer naming architecture used by top companies
  • Scale conventions (when to use numbers vs. words)
  • Dark mode naming strategies
  • Real-world examples from Material Design, Tailwind, and IBM
  • Common mistakes and how to avoid them

Why Color Naming Matters More Than You Think

Colors aren't just aesthetic choices—they're communication tools. When you name a color error-red, you're not just describing its appearance; you're defining its purpose. This distinction is crucial for scalable design systems.

❌ Descriptive Naming (Problematic)

  • blue-500
  • light-gray
  • dark-blue
  • brand-color
  • button-hover

Problems: Tied to appearance (breaks in dark mode), doesn't convey purpose, hard to maintain, creates ambiguity.

✅ Semantic Naming (Recommended)

  • color.primary.default
  • color.background.subtle
  • color.text.inverse
  • color.brand.accent
  • color.interactive.hover

Benefits: Purpose-driven, theme-agnostic, scalable, self-documenting, team-aligned.

The Three-Layer Naming Architecture

The most robust color naming systems use a three-layer architecture that separates concerns and enables flexibility:

Layer 1: Primitive Colors (The Foundation)

Primitive colors are your raw color values—the actual hex, RGB, or HSL values. These should be named descriptively but kept minimal:

// Primitive color palette
color.primitive.blue.100: #eff6ff
color.primitive.blue.200: #dbeafe
color.primitive.blue.300: #bfdbfe
color.primitive.blue.400: #93c5fd
color.primitive.blue.500: #60a5fa
color.primitive.blue.600: #3b82f6
color.primitive.blue.700: #2563eb
color.primitive.blue.800: #1d4ed8
color.primitive.blue.900: #1e40af

color.primitive.gray.50: #f9fafb
color.primitive.gray.100: #f3f4f6
color.primitive.gray.200: #e5e7eb
color.primitive.gray.300: #d1d5db
color.primitive.gray.400: #9ca3af
color.primitive.gray.500: #6b7280
color.primitive.gray.600: #4b5563
color.primitive.gray.700: #374151
color.primitive.gray.800: #1f2937
color.primitive.gray.900: #111827

Key principles for primitives:

  • Use numeric scales (50-900) for consistency
  • Group by hue family (blue, gray, red, green, etc.)
  • Keep this layer small—only colors you actually need
  • Never use primitives directly in components

Layer 2: Semantic Tokens (The Meaning)

Semantic tokens map primitives to purposes. This is where the magic happens:

// Semantic color tokens
color.background.primary: color.primitive.gray.50
color.background.secondary: color.primitive.gray.100
color.background.subtle: color.primitive.gray.200

color.text.primary: color.primitive.gray.900
color.text.secondary: color.primitive.gray.600
color.text.subtle: color.primitive.gray.500
color.text.inverse: color.primitive.gray.50

color.border.default: color.primitive.gray.300
color.border.subtle: color.primitive.gray.200
color.border.strong: color.primitive.gray.400

color.primary.default: color.primitive.blue.600
color.primary.hover: color.primitive.blue.700
color.primary.active: color.primitive.blue.800
color.primary.subtle: color.primitive.blue.100

color.success.default: color.primitive.green.600
color.warning.default: color.primitive.amber.500
color.error.default: color.primitive.red.600
color.info.default: color.primitive.blue.600

Why this matters: When you need to support dark mode, you only change the semantic layer—not every component:

// Dark mode overrides (semantic layer only)
color.background.primary: color.primitive.gray.900
color.background.secondary: color.primitive.gray.800
color.background.subtle: color.primitive.gray.700

color.text.primary: color.primitive.gray.50
color.text.secondary: color.primitive.gray.300
color.text.subtle: color.primitive.gray.400
color.text.inverse: color.primitive.gray.900

color.border.default: color.primitive.gray.700
color.border.subtle: color.primitive.gray.800
color.border.strong: color.primitive.gray.600

Layer 3: Component-Specific Aliases (The Context)

For complex components, you may need a third layer that provides component-specific context:

// Component-specific color aliases
button.primary.background: color.primary.default
button.primary.background.hover: color.primary.hover
button.primary.text: color.text.inverse

button.secondary.background: color.background.primary
button.secondary.background.hover: color.background.secondary
button.secondary.border: color.border.default

alert.error.background: color.error.subtle
alert.error.border: color.error.default
alert.error.icon: color.error.default
alert.error.text: color.text.primary

When to use this layer:

  • When a component needs very specific color control
  • When the same semantic token needs different meanings in different contexts
  • When migrating legacy components to a new design system

💡 Pro Tip: Start Simple

Don't over-engineer from day one. Start with primitives and semantic tokens. Add component aliases only when you actually need them. Most projects never need layer 3.

Scale Conventions: Numbers vs. Words

One of the most common questions: should you use numbers (500) or words (default, light, dark)?

Use Numbers When:

  • You have a scale with 5+ steps—numbers provide clear ordering
  • You need fine-grained controlblue-400 vs blue-500 is more precise than blue-light
  • You're building a primitive palette—Tailwind, Material Design, and IBM all use numeric scales
50
100
200
300
400
500
600
700
800
900

Use Words When:

  • You have 3-4 steps maxdefault, hover, active is clearer than 500, 600, 700
  • The meaning is functional, not visualcolor.text.primary is better than color.text.900
  • You're naming semantic tokens—purpose over appearance

Recommended Hybrid Approach:

// Primitives: Numbers (visual scale)
color.primitive.blue.500: #60a5fa

// Semantic: Words (purpose-driven)
color.primary.default: color.primitive.blue.500
color.primary.hover: color.primitive.blue.600
color.primary.active: color.primitive.blue.700

// Backgrounds: Words (functional)
color.background.primary: #ffffff
color.background.secondary: #f3f4f6
color.background.subtle: #e5e7eb

Dark Mode Naming Strategies

Dark mode doesn't require a separate naming scheme—that's the beauty of semantic tokens. However, there are some considerations:

Strategy 1: Automatic Inversion (Recommended)

Keep the same token names, change the values per theme:

// Light theme
color.background.primary: #ffffff
color.text.primary: #111827

// Dark theme (same tokens, different values)
color.background.primary: #111827
color.text.primary: #f9fafb

Benefits: No code changes needed, consistent API, easier maintenance.

Strategy 2: Explicit Mode Suffixes (Avoid)

// DON'T do this
color.background.light: #ffffff
color.background.dark: #111827

color.text.light: #111827
color.text.dark: #f9fafb

Why this fails: What happens when you add a third theme (high contrast, sepia, etc.)? You'll end up with color.background.high-contrast, color.background.sepia, and your naming scheme breaks.

Strategy 3: Contextual Overrides

For colors that need to behave differently in dark mode (not just invert):

// Primary brand color stays consistent
color.primary.default: #3b82f6 (both modes)

// But backgrounds adapt
color.background.surface: #ffffff (light)
color.background.surface: #1f2937 (dark)

// Shadows only exist in light mode
color.shadow.default: rgba(0,0,0,0.1) (light)
color.shadow.default: transparent (dark)

Real-World Examples from Industry Leaders

Material Design 3

// Material Design color tokens
md.sys.color.primary
md.sys.color.on-primary
md.sys.color.primary-container
md.sys.color.on-primary-container
md.sys.color.secondary
md.sys.color.tertiary
md.sys.color.error
md.sys.color.background
md.sys.color.on-background
md.sys.color.surface
md.sys.color.on-surface

Key insight: Material uses "on-" prefixes to indicate text colors that should appear on top of a background.

Tailwind CSS

// Tailwind's pragmatic approach
bg-blue-500
text-gray-900
border-red-300
hover:bg-blue-600
dark:bg-gray-900

Key insight: Tailwind keeps it simple with descriptive names but uses utility classes for context.

IBM Carbon Design System

// IBM's comprehensive system
$ibm-color__blue-60
$ibm-color__primary
$ibm-color__inverse
$ibm-color__ui-background
$ibm-color__text-primary
$ibm-color__link-primary

Key insight: IBM combines numeric primitives with semantic tokens for maximum flexibility.

Common Mistakes and How to Avoid Them

Mistake 1: Naming Colors After Their Appearance

// ❌ Bad
$light-blue: #60a5fa
$dark-blue: #1e40af

// ✅ Good
$color-primary-default: #60a5fa
$color-primary-dark: #1e40af

Why: What happens when your "light blue" becomes the primary brand color? Or when you need a darker primary for accessibility?

Mistake 2: Creating One Token Per Use Case

// ❌ Bad
$button-primary-bg
$button-secondary-bg
$link-color
$heading-color
$alert-background
$card-border

// ✅ Good
$color-primary-default
$color-secondary-default
$color-text-primary
$color-background-primary
$color-border-default

Why: You'll end up with hundreds of tokens. Semantic tokens should be reusable across contexts.

Mistake 3: Ignoring Accessibility in Naming

// ❌ Bad
$color-gray-400: #9ca3af  // Fails WCAG on white

// ✅ Good
$color-text-subtle: #6b7280  // Passes WCAG AA
$color-text-subtle-large: #9ca3af  // Only for large text

Why: Bake accessibility into your naming. If a color doesn't meet contrast requirements, name it accordingly.

Mistake 4: Not Planning for Expansion

// ❌ Bad (hardcoded scale)
$color-blue-light
$color-blue-medium
$color-blue-dark

// ✅ Good (expandable scale)
$color-blue-100
$color-blue-200
$color-blue-300
// ... can add 400, 500, etc. later

Implementation Checklist

Ready to implement a robust color naming system? Follow this checklist:

  1. Audit existing colors—list every color currently in use
  2. Define your primitive palette—choose 5-8 hue families with 50-900 scales
  3. Map semantic tokens—identify all use cases (backgrounds, text, borders, interactive states)
  4. Document the system—create a living document explaining naming conventions
  5. Build theme support—ensure tokens work for light/dark modes
  6. Migrate incrementally—start with new components, gradually refactor legacy code
  7. Enforce with tooling—use linting rules to prevent primitive usage in components

🎨 ColorPick Integration

Use ColorPick to visualize and export your color tokens in multiple formats (CSS, SCSS, JavaScript, design tokens). Our design token exporter supports semantic naming out of the box.

Conclusion

Good color naming is an investment that pays dividends as your design system grows. The upfront effort of thinking semantically, planning for themes, and documenting conventions will save countless hours of confusion and refactoring down the road.

Remember the key principles:

  • Semantic over descriptive—name colors for their purpose, not appearance
  • Three-layer architecture—primitives, semantic tokens, component aliases
  • Theme-agnostic—same token names, different values per theme
  • Hybrid scales—numbers for primitives, words for semantic tokens
  • Document everything—your future self (and teammates) will thank you

Start small, iterate often, and build a color naming system that scales with your team.


About ColorPick: ColorPick is a comprehensive color tool for designers and developers. Create palettes, convert formats, check accessibility, and export design tokens—all in one place. Try it free at colorpick.app.