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-500light-graydark-bluebrand-colorbutton-hover
Problems: Tied to appearance (breaks in dark mode), doesn't convey purpose, hard to maintain, creates ambiguity.
✅ Semantic Naming (Recommended)
color.primary.defaultcolor.background.subtlecolor.text.inversecolor.brand.accentcolor.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 control—
blue-400vsblue-500is more precise thanblue-light - You're building a primitive palette—Tailwind, Material Design, and IBM all use numeric scales
Use Words When:
- You have 3-4 steps max—
default,hover,activeis clearer than500,600,700 - The meaning is functional, not visual—
color.text.primaryis better thancolor.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:
- Audit existing colors—list every color currently in use
- Define your primitive palette—choose 5-8 hue families with 50-900 scales
- Map semantic tokens—identify all use cases (backgrounds, text, borders, interactive states)
- Document the system—create a living document explaining naming conventions
- Build theme support—ensure tokens work for light/dark modes
- Migrate incrementally—start with new components, gradually refactor legacy code
- 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.