PaletteGen

Design Tokens Explained: Primitives vs. Semantic Color Tokens

Naming your colors --primary and --gray-500 both work. Knowing when to use which is what separates a color system that scales from one that turns into a pile of one-off values.

Design tokens are named values that store a design decision — a color, a spacing value, a font size — instead of hardcoding it directly wherever it's used. Type --primary-500 instead of #0891B2, and updating your brand color later means changing one value instead of hunting through every file that used the raw hex code.

Adoption of this pattern has grown fast: a zeroheight industry survey of around 300 design professionals found adoption at 84% of teams, up from 56% the year before, and the W3C Design Tokens Community Group shipped its first stable file format (the Design Tokens Format Module, version 2025.10) on October 28, 2025, backed by 24+ organizations including Adobe, Google, Meta, and Figma. The idea itself isn't new. What changed is that there's now one shared, stable format instead of every design tool exporting its own incompatible version.

The two layers: primitives and semantic tokens

Primitive tokens

The raw palette. A primitive token is just a named color with no meaning attached — --blue-500, --gray-100, --red-600. This is the scale you'd get straight out of a palette generator: a base hue at several lightness steps, nothing more.

--blue-500 #0891B2

Semantic tokens

A semantic token names intent instead of a raw value, and points at a primitive token to get its actual color. --action-color points at --blue-500. --text-error points at --red-600. The name describes what the color is for, not what it looks like.

--action-color --blue-500 #0891B2

This indirection is the entire point. When a dark theme needs a lighter version of your action color, you re-point the semantic token at a different primitive — --action-color now points at --blue-300 instead of --blue-500 — and every component that used --action-color updates automatically, without anyone touching the components themselves. This is the same underlying idea behind this site's own light-dark() generator and its companion guide on choosing dark mode colors, just applied one layer up: those pages help you pick the right value for a single token in two themes, this is the naming structure that lets a whole system of tokens stay organized as it grows past a handful of colors.

Why the split matters in practice

Without semantic tokens, a color system tends to accumulate the same problem over time: a designer updates the brand color in one place, but developers have hardcoded the raw hex value in six different files, so the update doesn't propagate. Marketing needs a slightly different accent for one campaign, and instead of reusing an existing token, a new one-off value gets added. Six months later, nobody's sure which of the dozen blue-ish hex codes in the codebase is actually the current brand color.

The semantic layer is what prevents this. If every component only ever references --action-color and never a raw hex value, changing the brand color really is a one-line change. The primitive layer still exists underneath — you need somewhere for --action-color to point — but nothing outside the token file should ever reference a primitive directly.

Starting your own token set

  1. Generate a base color scale (a primitive tier) using a palette generator or by picking lightness steps from a single brand hue.
  2. Name a small set of semantic tokens for the roles your interface actually needs: a background, a text color, an action/accent color, and state colors (success, warning, danger) if you use them. Don't invent semantic names you don't have a use for yet.
  3. Point each semantic token at a primitive. Keep this mapping in one place, not scattered across files.
  4. For a light/dark pair specifically, build the actual CSS with the light-dark() generator, which now includes a button that suggests a starting dark value for any primitive you give it.

Frequently Asked Questions

Do I need semantic tokens for a small project?

Not necessarily. A handful of primitive colors used consistently is fine for a small site. Semantic tokens start paying for themselves once you have multiple themes, multiple people touching the codebase, or a color system that's grown past a dozen or so values.

What's the difference between a design token and a CSS custom property?

A CSS custom property (a --variable) is one way to implement a design token in the browser. "Design token" is the broader concept — the same token can also export to a Tailwind config, an iOS asset catalog, or an Android XML file, not just CSS.

Should every component only ever use semantic tokens, never primitives?

That's the goal in a mature system. It keeps the "what changed and why did it break something unrelated" problem contained to the token-mapping file instead of spread across every component that happens to reference a color directly.

How is this different from just naming variables well?

It isn't fundamentally different in spirit — the primitive/semantic split is really just a disciplined naming convention. What's changed recently is that a shared file format now exists for it, so the same token definitions can drive multiple tools instead of being hand-translated for each one.

Sources

About this page

PaletteGen is built and maintained by Cedrick Reese at Ready Utilities. Formulas and technical claims on this page are checked against the primary sources listed above. Reviewed and updated: August 30, 2026. See the full About page for more.