DEV Community

Cover image for How to Build a Color System for Your Design System
Ali Badshah
Ali Badshah

Posted on

How to Build a Color System for Your Design System

A design system usually starts with components.

Buttons. Inputs. Cards. Navigation. Modals.

But before those components become reusable, there is something underneath almost all of them that needs to be consistent: color.

If every component uses slightly different shades of the same color, the interface starts feeling inconsistent very quickly.

You might have five different grays for borders, three different blues for buttons, and several versions of the same green scattered across your CSS.

It works at first.

Then the project grows.

That's when a proper color system starts becoming extremely useful.

In this article, I'll walk through a practical way to turn a handful of colors into a reusable system that developers and designers can actually work with.


Start With a Small Color Foundation

You don't need 50 colors to create a good design system.

In fact, starting with too many colors can make things harder.

I usually begin with a small foundation:

Primary
Secondary
Neutral
Success
Warning
Error
Enter fullscreen mode Exit fullscreen mode

For example, let's say the main brand color is:

#117A6A
Primary

From this one color, we can create a useful range of shades.

:root {
  --primary-50:  #F1FAF8;
  --primary-100: #DFF3EF;
  --primary-200: #BDE5DE;
  --primary-300: #8CCDC3;
  --primary-400: #5BB5A7;
  --primary-500: #117A6A;
  --primary-600: #0F6E60;
  --primary-700: #0B5B50;
  --primary-800: #08483F;
  --primary-900: #063C35;
}
Enter fullscreen mode Exit fullscreen mode

Now the design system has a predictable range instead of one isolated color.


Why Color Scales Are Better Than Random Shades

Imagine a developer needs a slightly darker version of the primary button.

Without a color system, they might choose:

background: #0C665A;
Enter fullscreen mode Exit fullscreen mode

Another developer might choose:

background: #0A6156;
Enter fullscreen mode Exit fullscreen mode

A designer might later choose:

background: #0E6B5F;
Enter fullscreen mode Exit fullscreen mode

All three may look almost identical.

But now the project has three different colors doing roughly the same job.

That's how color systems slowly become messy.

With a predefined scale, everyone knows where to look:

Primary 500 → Default
Primary 600 → Hover
Primary 700 → Active
Primary 300 → Subtle accent
Primary 100 → Light background
Primary 50  → Very light surface
Enter fullscreen mode Exit fullscreen mode

The exact mapping can change depending on the project, but the principle stays the same.

Give your colors predictable jobs.


Color palette showing a complete range of primary, neutral and semantic colors


Neutrals Are Part of the System Too

When people talk about design systems, they often focus on brand colors.

But neutrals can actually be used much more frequently.

Think about a normal dashboard.

You might need colors for:

  • Page background
  • Cards
  • Borders
  • Dividers
  • Primary text
  • Secondary text
  • Disabled text
  • Input backgrounds
  • Hover states

A small neutral scale can handle most of this.

:root {
  --neutral-50:  #F8FAF9;
  --neutral-100: #EEF3F1;
  --neutral-200: #DDE5E2;
  --neutral-300: #C6D0CD;
  --neutral-400: #9AA7A3;
  --neutral-500: #7B8985;
  --neutral-600: #66736F;
  --neutral-700: #4C5855;
  --neutral-800: #303B38;
  --neutral-900: #17201E;
}
Enter fullscreen mode Exit fullscreen mode

Now instead of creating a new gray whenever you need one, you can choose from an established scale.

A simple neutral system

Token Example Typical use
neutral-50 #F8FAF9 Page background
neutral-100 #EEF3F1 Subtle surfaces
neutral-200 #DDE5E2 Borders
neutral-500 #7B8985 Secondary UI
neutral-600 #66736F Muted text
neutral-900 #17201E Main text

The advantage is consistency.

A border on one page can use the same token as a border somewhere else.


Then Add Semantic Colors

Brand colors and neutral colors aren't enough.

Most products also need colors that communicate states.

For example:

Success
Warning
Error
Information
Enter fullscreen mode Exit fullscreen mode

A basic system might look like this:

Success Warning Error Info

These colors should not be chosen just because they look nice.

They need to be recognizable and work consistently across the interface.

For example:

:root {
  --success: #238636;
  --warning: #D29922;
  --error: #CF222E;
  --info: #2563EB;
}
Enter fullscreen mode Exit fullscreen mode

Now an alert component can use the semantic token rather than a hard-coded color.

.alert-success {
  color: var(--success);
}

.alert-warning {
  color: var(--warning);
}

.alert-error {
  color: var(--error);
}
Enter fullscreen mode Exit fullscreen mode

This is much easier to maintain.


Separate Color Values From Color Roles

This is one of the most useful changes you can make to a design system.

Instead of only having:

--green-500: #117A6A;
Enter fullscreen mode Exit fullscreen mode

you can create a second layer that describes what the color is doing.

For example:

:root {
  --green-500: #117A6A;

  --color-brand: var(--green-500);
  --color-action-primary: var(--green-500);
  --color-link: var(--green-500);
}
Enter fullscreen mode Exit fullscreen mode

Now the component doesn't necessarily care that the color is green.

It cares that the color represents a primary action.

That distinction becomes extremely valuable if the brand changes later.


Design system showing reusable buttons, cards, inputs and semantic color states


Build Components Around the Color System

Once the tokens exist, components become easier to create.

Take a button.

Instead of this:

button {
  background: #117A6A;
  color: white;
}
Enter fullscreen mode Exit fullscreen mode

you can use:

button {
  background: var(--color-action-primary);
  color: var(--color-text-on-primary);
}
Enter fullscreen mode Exit fullscreen mode

Then define the tokens:

:root {
  --color-action-primary: #117A6A;
  --color-text-on-primary: #FFFFFF;
}
Enter fullscreen mode Exit fullscreen mode

The component now depends on the system instead of depending on one specific hex code.

That might seem like a small difference.

It isn't.

When you have dozens or hundreds of components, this separation can save a lot of time.


What About Dark Mode?

This is another reason semantic tokens are useful.

Suppose your light theme contains:

:root {
  --color-background: #F8FAF9;
  --color-surface: #FFFFFF;
  --color-text: #17201E;
}
Enter fullscreen mode Exit fullscreen mode

You can create a dark theme without changing every component individually.

[data-theme="dark"] {
  --color-background: #101614;
  --color-surface: #17201E;
  --color-text: #F4F7F6;
}
Enter fullscreen mode Exit fullscreen mode

Your component can continue using:

.card {
  background: var(--color-surface);
}

body {
  background: var(--color-background);
  color: var(--color-text);
}
Enter fullscreen mode Exit fullscreen mode

The component doesn't need to know whether the user is looking at light mode or dark mode.

The theme handles it.


Don't Forget Accessibility

A color system isn't complete just because the colors look good together.

You also need to check how those colors behave with actual text and interface elements.

For example, a muted text color might look elegant in a design file but become difficult to read on a real background.

Before finalizing a token, test combinations such as:

Text + Background
Button text + Button background
Link + Background
Placeholder + Input background
Focus state + Surface
Error text + Error background
Enter fullscreen mode Exit fullscreen mode

And don't use color as the only way to communicate a state.

Instead of:

🔴
Enter fullscreen mode Exit fullscreen mode

use:

⚠ Payment failed

Please check your payment information and try again.
Enter fullscreen mode Exit fullscreen mode

The color reinforces the message instead of being the entire message.


How I Organize Color Tokens

For a smaller project, I usually keep the system fairly simple.

:root {

  /* Brand */
  --brand-500: #117A6A;
  --brand-600: #0F6E60;
  --brand-700: #0B5B50;

  /* Neutral */
  --neutral-50: #F8FAF9;
  --neutral-100: #EEF3F1;
  --neutral-200: #DDE5E2;
  --neutral-600: #66736F;
  --neutral-900: #17201E;

  /* Semantic */
  --success: #238636;
  --warning: #D29922;
  --error: #CF222E;
  --info: #2563EB;
}
Enter fullscreen mode Exit fullscreen mode

Then I create role-based tokens:

:root {

  --color-background: var(--neutral-50);
  --color-surface: #FFFFFF;

  --color-text: var(--neutral-900);
  --color-text-muted: var(--neutral-600);

  --color-border: var(--neutral-200);

  --color-primary: var(--brand-500);
  --color-primary-hover: var(--brand-600);

  --color-success: var(--success);
  --color-warning: var(--warning);
  --color-error: var(--error);
  --color-info: var(--info);
}
Enter fullscreen mode Exit fullscreen mode

This gives you two useful layers:

RAW COLOR
   ↓
COLOR SCALE
   ↓
SEMANTIC ROLE
   ↓
COMPONENT
Enter fullscreen mode Exit fullscreen mode

For example:

#117A6A
   ↓
brand-500
   ↓
color-primary
   ↓
primary-button
Enter fullscreen mode Exit fullscreen mode

That structure makes the system much easier to understand.


You Don't Have to Create Every Palette Manually

The difficult part isn't always writing the CSS.

Sometimes the difficult part is deciding which colors should actually belong in the system.

That's where palette tools can help.

You can start with a single brand color and explore different combinations before turning them into design tokens.

For example, ColorFiind can be useful during the exploration stage when you want to experiment with color combinations and build a starting palette.

I wouldn't treat a generated palette as the final answer.

I'd use it as a starting point.

The actual UI still needs to be tested with real content, real components and real accessibility requirements.


A Practical Color-System Workflow

If you're starting a new project, here's a workflow that works well:

Choose Brand Color
       ↓
Create Color Scale
       ↓
Create Neutral Scale
       ↓
Add Semantic Colors
       ↓
Define Semantic Roles
       ↓
Create Design Tokens
       ↓
Build Components
       ↓
Test Contrast
       ↓
Test Light / Dark Themes
       ↓
Document the System
Enter fullscreen mode Exit fullscreen mode

The important thing is to do this before every component starts inventing its own colors.


Final Thoughts

A color system doesn't need to be complicated.

You can start with a primary color, a neutral scale and a few semantic colors.

The real improvement comes from giving those colors consistent roles.

Instead of asking:

"Which hex code should I use here?"

you can ask:

"Which color role belongs here?"

That's a much easier question to answer when your project has dozens of components.

And once the system is defined, designers and developers can work from the same visual language.

The goal isn't to have more colors. The goal is to make the colors you already have easier to use.

If you're starting from scratch, explore a few palette directions first, choose what actually works in your UI, and then turn those decisions into reusable tokens.

Explore ColorFiind and build your next palette →

Top comments (0)