The Color System I Wish I Understood Sooner

Does your system for naming colors always feel like it falls short? This two-layer approach solves so many problems!

Published:

Filed Under:

Design

Kyle Van Deusen

The Admin Bar

After spending 15 years as a graphic designer and earning a business degree, I launched my agency, OGAL Web Design, in 2017. A year later, after finding the amazing community around WordPress, I co-found The Admin Bar, which has grown to become the #1 community for WordPress professionals. I'm a husband and proud father of three, and a resident of the Commonwealth of Virginia.

Diagram illustrating color categories, including primary, secondary, and neutral tones, for design u.

This content contains affiliate links. View our affiliate disclaimer.

Here’s something most website builders don’t think about until it bites them: the way you manage color on a project has a massive impact on how easy (or miserable) that project is to maintain.

Not just at launch. Six months later, when a client wants to tweak their brand color. A year later, when you’re adding a new section and can’t remember what shade of blue you used on the buttons. Two years later, when someone else entirely has to touch the project and has no idea what’s going on.

Color management is one of those things that feels fine when you’re flying through a build, and only reveals its problems when something needs to change.

This post is about a two-layer approach to color (using primitives and semantics together) that solves problems you might not even know you have yet. But to understand why it works, we need to start with why the simpler approaches fall short.

Prefer watching? This post is available in video form!

YouTube video

The problem: 16 million colors is too many choices

Did you know there are over 16 million possible color values in HEX alone?

That’s not a fun fact. That’s the problem. Every time you open a color picker, that’s technically how many options you’re choosing from. It’s no wonder color decisions feel hard. Crayola doesn’t make a 16 million piece set because you wouldn’t know where to start. But that’s essentially what our tools hand us.

So the natural response is to build a palette. A curated, intentional set of colors for the project, instead of reaching into the void every time you need to pick something.

That’s the right instinct. The question is how you build and manage that palette. And this is where most people, without realizing it, take a wrong turn.

A palette is more than you think

Before we get into the approaches, it’s worth acknowledging that a project palette isn’t just your brand colors. It’s a lot more than that.

For any real website, you’re going to need:

  • Neutral colors for backgrounds, borders, and body text
  • Brand colors — your primary and secondary
  • Utility colors for success states, warnings, and errors
  • Shades of all of the above for hover states, disabled states, subtle accents, and more

That adds up to a lot of colors. And the more colors you have, the harder they are to manage consistently. Especially when you’re working in a visual builder and there’s no automated enforcement keeping things in line.

Approach 1: Primitives

A primitive palette is the most common first step designers take when they want to get more organized. Instead of grabbing random HEX values whenever you need a color, you define a structured set of named colors upfront.

Tailwind CSS is a great real-world example. Their palette has 22 color families — blue, red, green, orange, gray, and so on — each with 11 shades numbered from 100 (lightest) to 950 (darkest). Names like blue-500, gray-200, red-900.

That takes you from 16 million possible values down to 242 named options. A massive improvement. And for a typical project, you’d probably only use 6-8 of those color families, which narrows it further.

In CSS using custom properties, a primitive palette looks like this:

:root {

  /* Blue */
  --blue-100: #dbeafe;
  --blue-500: #3b82f6;
  --blue-600: #2563eb;
  --blue-700: #1d4ed8;
  --blue-800: #1e40af;
  --blue-900: #1e3a8a;

  /* Gray */
  --gray-100: #f3f4f6;
  --gray-200: #e5e7eb;
  --gray-300: #d1d5db;
  --gray-500: #6b7280;
  --gray-800: #1f2937;
  --gray-900: #111827;

}

Clean, organized, consistent. Every color value lives in one place.

Where primitives fall short

Here is the problem: a primitive palette tells you what colors exist, but it tells you nothing about where they are used.

Six months into a project, you need to update the button color. You open your builder and stare at your palette. Which blue was it? You used --blue-600 on most buttons, but you also have a secondary button style. Was that --blue-400? Or --blue-500? And what about the links? Were those --blue-600 too, or a different shade?

Primitives are better than raw HEX values. But the names don’t carry any meaning about how they’re applied. You still have to remember (or reverse-engineer) what goes where.

“Did I put --blue-900 on the buttons? Or was it --blue-700?”

If you’ve ever caught yourself asking that question, you’ve hit the ceiling of a primitives-only approach.

Approach 2: Semantics

Semantic color naming takes a completely different approach. Instead of naming colors by their visual properties (blue-600), you name them by their purpose in the design.

Instead of --blue-600, you have --button-background. Instead of --gray-200, you have --border-subtle. Instead of --gray-900, you have --heading.

In practice, it looks like this:

:root {

  --button-background: #2563eb;
  --button-background-hover: #1d4ed8;
  --button-text: #f3f4f6;

  --link-color: #2563eb;
  --link-color-hover: #1e40af;

  --text: #1f2937;
  --text-muted: #6b7280;
  --heading: #111827;

  --background: #f3f4f6;
  --border: #d1d5db;

  --cta-background: #2563eb;
  --cta-text: #f3f4f6;

  --color-success: #16a34a;
  --color-warning: #f97316;
  --color-error: #dc2626;

}

Now when you need to update the button color, you go straight to –button-background. No guessing. No hunting.

The name tells you exactly what it is.

Where semantics fall short

Here’s the fatal flaw: in a real project, many of your semantic colors will share the same value.

Your button background, your links, your CTA backgrounds, and your icon color might all be the exact same blue. They should be, for consistency. So in a semantics-only palette, you’d have:

--button-background: #2563eb;
--link-color: #2563eb;
--cta-background: #2563eb;
--icon-color: #2563eb;

Four separate variables. All set to the same HEX value. All disconnected from each other.

Now imagine your client tells you that blue is a touch too bright and they want to shift it slightly. You have to find every semantic variable using that HEX value and update them one by one. Miss one (and it’s easy to miss one) and your colors are now out of sync. Buttons are one shade of blue. Links are a slightly different shade. CTAs are the original. The project looks subtly broken in a way that’s hard to pinpoint.

Nothing ties those repeated values together. They’re just scattered across your stylesheet, independent and invisible to each other.

Why neither approach is enough on its own

Let’s take stock:

  • Primitives give you an organized, consistent palette… but the names mean nothing about how the colors are used, so you’re constantly guessing what goes where.
  • Semantics give you meaningful names that make your work intuitive… but repeated HEX values have nothing connecting them, so global changes become a tedious, error-prone hunt.

Both approaches feel like they should work. And they do — up to a point. Primitives work fine when your project is small and you remember everything. Semantics work fine when nothing ever needs to change.

But projects grow. Colors change. Clients call.

And when that happens, both approaches reveal the same fundamental limitation: they’re trying to solve two different problems with one layer, and one layer can’t do both jobs well.

The two-layer approach: primitives + semantics together

Primitives and semantics aren’t competing approaches. They’re complementary layers. When you use them together, each one does the job it’s actually good at.

Primitives define your palette: every color and shade the project is allowed to use, each pointing directly to a HEX value. This is your single source of truth for color values.

Semantics define how your palette is applied: human-friendly names for every element in your project, each pointing not to a HEX value, but to one of your primitives.

/* PRIMITIVES — your palette */
:root {

  --blue-600: #2563eb;
  --blue-700: #1d4ed8;
  --blue-800: #1e40af;
  --orange-600: #ea580c;
  --gray-100: #f3f4f6;
  --gray-200: #e5e7eb;
  --gray-300: #d1d5db;
  --gray-500: #6b7280;
  --gray-800: #1f2937;
  --gray-900: #111827;
  --green-600: #16a34a;
  --red-600: #dc2626;

}

/* SEMANTICS — how the palette is applied */
:root {

  --button-background: var(--blue-600);
  --button-background-hover: var(--blue-700);
  --button-text: var(--gray-100);

  --link-color: var(--blue-600);
  --link-color-hover: var(--blue-800);

  --text: var(--gray-800);
  --text-muted: var(--gray-500);
  --heading: var(--gray-900);

  --background: var(--gray-100);
  --background-subtle: var(--gray-200);
  --border: var(--gray-300);

  --cta-background: var(--blue-600);
  --cta-text: var(--gray-100);

  --color-success: var(--green-600);
  --color-warning: var(--orange-500);
  --color-error: var(--red-600);

}

Notice what’s happening: --button-background, --link-color, and --cta-background all point to --blue-600.

They share the same primitive — but they’re each their own independent semantic.

How this solves the primitives problem

With semantics on top of your primitives, you never need to remember whether buttons use --blue-600 or --blue-700. You just use --button-background and let the system handle the rest. The name tells you exactly what it is and where it belongs.

How this solves the semantics problem

With primitives as the foundation, a global color change becomes trivial. If your blue needs to shift, you update --blue-600 in one place, and --button-background, --link-color, and --cta-background all update automatically, because they’re all pointing to the same primitive.

The bonus: independent control

Because each semantic is its own variable, you can change individual elements independently without touching anything else.

Your client calls. Buttons should be orange.

One change:

--button-background: var(--orange-600);

That’s it. Only buttons change.

Links stay blue. CTAs stay blue. Everything else remains exactly where it was, still drawing from your defined primitives, still fully within your palette.

What you actually get

By using primitives and semantics together, you get:

  1. A defined, intentional palette. Primitives limit your project to a specific set of colors. Every color value lives in one place and nowhere else.
  2. Names that actually mean something. Semantics make your work intuitive. You always know what you’re looking for, without reverse-engineering what a number means.
  3. Global control and element-level flexibility at the same time. Update a primitive and everything using it follows. Update a semantic and only that element moves. You get both, without compromise.

Where you’ve probably been

Chances are you’ve experimented with both approaches. Maybe you’ve even unintentionally mixed the two. And if you haven’t seen how they benefit from working together, you’ve probably run into every limitation we talked about… and it’s frustrating, because both approaches feel like they should work on their own.

It took me a few attempts before this finally clicked. I’ve got projects out there using one, the other, or a messy combination of both. But once you separate the two layers and let them work together, everything gets more consistent, less messy, and a whole lot easier to manage.

Kyle Van Deusen

The Admin Bar

After spending 15 years as a graphic designer and earning a business degree, I launched my agency, OGAL Web Design, in 2017. A year later, after finding the amazing community around WordPress, I co-found The Admin Bar, which has grown to become the #1 community for WordPress professionals. I'm a husband and proud father of three, and a resident of the Commonwealth of Virginia.

Come Join Us!

Join the #1 WordPress Community and dive into conversations covering every aspect of running an agency!

Kyle Van Deusen

Community Manager

Latest Events

February, 26th, 2026

Code Snippets Are Slowing Down Your Website

Why Perfmatters Built a Performance-First Snippet Manager

September 16, 2025

Termageddon 2.0

Better Tools, Smoother Workflows, Happier Clients
Tpdc onblue

Learn a proven discovery framework to transform casual leads into high-paying clients.

View the Course
The Friday Chaser

Wash down the week with the best of The Admin Bar! News, tips, and the best conversations delivered straight to your inbox every Friday!

More Articles

Digital screen showing a cursor clicking the "Reject" button for privacy preferences.

They’re Here: Privacy-Savvy Website Users

81% of Americans distrust how websites use their data. Here’s what web designers need to know about building trust in a privacy-first world.

Icon of a form plugin for WordPress websites.

The Best WordPress Form Plugin for Web Agencies

WordPress agency owners share the form plugins they actually build with — from complex multi-step builds to simple contact forms, and why switching is harder than it sounds.

Illustration of a padlock symbolizing website security and protection.

The Best WordPress Security Solution for Agencies

Agency owners share their security stacks — from edge-level protection to server hardening and WordPress-specific defenses.