How-to

Stop Using blue-500: Name Colors Semantically

Stop using 'blue-500' everywhere. Learn semantic color naming for design systems that scale, support dark mode, and make AI code generation actually work.

FramingUI Team5 min read

Your codebase has 47 shades of blue. You added most of them by picking whatever looked right at the time. Now your brand color is changing, and you're about to spend a day doing search-and-replace while hoping you don't miss anything.

This is what literal color naming costs. The fix isn't more discipline—it's a different naming system.

Literal vs. Semantic Naming

Literal names describe what a color looks like: blue-500, gray-700, #3B82F6. They're fine for a base palette, but they become a liability the moment you use them directly in components.

Semantic names describe what a color does: primary, text-secondary, surface-elevated, border-subtle. They communicate intent. When you read bg-surface-elevated in a component, you understand this is for elevated surfaces—modals, popovers, floating elements. When you read bg-gray-50, you have to guess.

The practical difference: with literal names, changing your brand color requires finding every instance of the old color and replacing it. With semantic names, you update one mapping and every component updates automatically.

The Two-Tier System

The most maintainable structure separates raw color values from semantic meaning.

Tier 1: Base palette

Raw color values with literal names. These are your building blocks—not used directly in components.

:root {
  --palette-blue-400: #60A5FA;
  --palette-blue-500: #3B82F6;
  --palette-blue-600: #2563EB;
  --palette-red-500:  #EF4444;
  --palette-red-600:  #DC2626;
  --palette-gray-50:  #F9FAFB;
  --palette-gray-100: #F3F4F6;
  --palette-gray-900: #111827;
}

Tier 2: Semantic aliases

Role-based names that reference tier 1 values. These are what you use in components.

:root {
  /* Brand */
  --color-primary:       var(--palette-blue-500);
  --color-primary-hover: var(--palette-blue-600);

  /* Text */
  --color-text-primary:   var(--palette-gray-900);
  --color-text-secondary: var(--palette-gray-600);
  --color-text-disabled:  var(--palette-gray-400);

  /* Surfaces */
  --color-surface-base:     var(--palette-gray-50);
  --color-surface-card:     #FFFFFF;
  --color-surface-elevated: #FFFFFF;

  /* Borders */
  --color-border-default: var(--palette-gray-200);
  --color-border-subtle:  var(--palette-gray-100);
  --color-border-strong:  var(--palette-gray-400);

  /* Feedback */
  --color-danger:         var(--palette-red-500);
  --color-danger-hover:   var(--palette-red-600);
  --color-danger-surface: #FEF2F2;
}

When you rebrand from blue to purple, you update tier 1:

--palette-blue-500: #8B5CF6;  /* now purple */

Everything that references --color-primary updates automatically. Nothing in your component files needs to change.

Dark Mode Without Manual Overrides

The semantic system makes dark mode structurally simple rather than manually exhausting.

Wrong approach:

<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white border-gray-200 dark:border-gray-700">

This works, but every component needs individual dark mode variants. You'll forget some. You'll get them wrong on others.

Right approach:

:root {
  --color-surface-card:  #FFFFFF;
  --color-text-primary:  #111827;
  --color-border-default: #E5E7EB;
}

[data-theme="dark"] {
  --color-surface-card:  #1F2937;
  --color-text-primary:  #F9FAFB;
  --color-border-default: #374151;
}
<div className="bg-[var(--color-surface-card)] text-[var(--color-text-primary)] border-[var(--color-border-default)]">

One attribute change on the root element re-themes the entire app. This scales to any number of themes—high contrast, colorblind-friendly, brand variants—without touching component code.

Semantic Categories Worth Knowing

Brand colors drive intentional actions: primary CTAs, active navigation states, focus rings, links. Variations include hover, active, and disabled states.

Feedback colors communicate system states: success confirmations, error messages, warnings, informational notices. Each needs text, background, and border variants—using one --color-error for both error text and error backgrounds leads to contrast problems.

Text colors establish content hierarchy. A three-level system covers most cases: primary (headings, body copy), secondary (metadata, descriptions), and disabled (inactive states). An inverse color for text on dark backgrounds completes the set.

Surface colors define background layers. Different surfaces have different visual weight: the page background, cards, elevated elements like modals and tooltips, and overlay backdrops each need a distinct token.

Border colors handle dividers, outlines, and focus rings. Subtle, default, and strong variants give you the range needed without over-specifying.

Common Mistakes

Too many shades in the semantic layer. If you have primary-50 through primary-900 as semantic tokens, developers don't know which one to use for a button background versus a hover state. Keep the semantic layer thin and specific: --color-primary, --color-primary-hover, --color-primary-surface. Let the base palette hold the full range.

Mixing naming systems. Having both --blue-500 and --color-primary with the same value creates confusion. Developers reach for whichever feels right in the moment. Pick one system and stick to it.

Component-specific token explosion. Creating --button-primary-background, --card-background, --modal-background, --input-background as separate tokens multiplies your surface area without benefit. Card backgrounds and modal backgrounds usually share the same surface color. Reference the semantic token and only create component-specific overrides when you genuinely need them.

Why This Matters for AI Code Generation

When AI tools generate UI components without design system context, they produce statistically likely color choices. bg-white for cards because that's common. text-gray-600 for secondary text because that's common. bg-red-500 for errors because that's common.

These choices aren't wrong in the abstract. They're wrong for your specific system.

With semantic tokens surfaced through an MCP connection, AI tools can query your design system before generating code. Instead of inferring that secondary text is probably text-gray-600, the AI reads your token definition: --color-text-secondary is mapped to a specific value, and that's what it uses. Generated components land in your system on the first attempt.

The token name carries the intent. An AI that knows --color-danger means destructive actions, not just any shade of red, will produce bg-[var(--color-danger)] in the right places—not wherever red seemed appropriate.

Semantic color naming is one of the higher-leverage structural choices you can make early in a project. The naming convention costs nothing to establish. It pays off every time you change a color, add a theme, or ask an AI to generate a component.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts