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
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;
}
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;
Another developer might choose:
background: #0A6156;
A designer might later choose:
background: #0E6B5F;
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
The exact mapping can change depending on the project, but the principle stays the same.
Give your colors predictable jobs.
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;
}
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
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;
}
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);
}
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;
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);
}
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.
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;
}
you can use:
button {
background: var(--color-action-primary);
color: var(--color-text-on-primary);
}
Then define the tokens:
:root {
--color-action-primary: #117A6A;
--color-text-on-primary: #FFFFFF;
}
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;
}
You can create a dark theme without changing every component individually.
[data-theme="dark"] {
--color-background: #101614;
--color-surface: #17201E;
--color-text: #F4F7F6;
}
Your component can continue using:
.card {
background: var(--color-surface);
}
body {
background: var(--color-background);
color: var(--color-text);
}
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
And don't use color as the only way to communicate a state.
Instead of:
🔴
use:
⚠ Payment failed
Please check your payment information and try again.
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;
}
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);
}
This gives you two useful layers:
RAW COLOR
↓
COLOR SCALE
↓
SEMANTIC ROLE
↓
COMPONENT
For example:
#117A6A
↓
brand-500
↓
color-primary
↓
primary-button
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
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.
Top comments (0)