How to Generate a Complete CSS Design Token System from a Single Brand Color
Generate a complete CSS variable set and semantic token mapping from one brand color
Design systems collapse in one predictable way: the color palette gets defined once, then grows through exception. A developer needs a slightly lighter button background. A designer exports a new shade from Figma. Someone hardcodes a hex value directly into a component because there was no token for the exact shade they needed. Six months later, the codebase has forty-something color values and no one knows which ones are canonical.
The standard solution is design tokens - a layer of named, semantic values that sit between your raw palette and your component code. The problem is that building a token system from scratch is tedious. You need a tonal scale for each color, a semantic mapping for each use case, and export formats that work with your toolchain. Most teams skip the systematic approach and pay for it every time they touch the theme.
This guide walks through how to build a proper token system from a single brand color, what decisions you need to make before generating anything, and where to use the tooling to handle the mechanical work.
Photo by Eathan Hood on Pexels
What Design Tokens Actually Are
A design token is a name-value pair where the name encodes meaning and the value encodes a specific design decision. The token --color-surface-primary is more useful than --color-white because it describes purpose rather than appearance. When you add dark mode, you remap --color-surface-primary to a dark shade without touching any component that uses it.
The Design Tokens Community Group at W3C has been working on a standard format for tokens since 2019. Their draft specification describes tokens in three tiers: raw values, primitive tokens (named palette entries), and semantic tokens (purpose-specific mappings). Understanding this three-tier structure is useful before you start generating anything, because the decisions you make at each tier propagate outward.
CSS implements the primitive and semantic token layers through custom properties, which have been in browsers since 2016. A custom property defined in :root is available to every element in the document. Child elements can override it locally. This inheritance model maps well to how a token system works: global palette values defined at root, component-local overrides applied closer to the elements that need them.
:root {
/* Primitive tokens - the raw palette */
--color-blue-50: hsl(220, 90%, 95%);
--color-blue-100: hsl(220, 90%, 88%);
--color-blue-500: hsl(220, 90%, 50%);
--color-blue-700: hsl(220, 90%, 30%);
--color-blue-900: hsl(220, 90%, 12%);
/* Semantic tokens - purpose-specific mappings */
--color-interactive: var(--color-blue-500);
--color-interactive-hover: var(--color-blue-700);
--color-surface-accent: var(--color-blue-50);
--color-text-on-accent: var(--color-blue-900);
}
This pattern, where semantic tokens reference primitive tokens by name rather than repeating their values, means a full palette update requires changing only the primitive layer.
The MDN reference on custom property inheritance covers how custom properties cascade and what happens when they are set to invalid values. The CSS Custom Properties specification is the authoritative source for browser behavior edge cases.
Step-by-Step: Generating a Token System
Step 1: Define Your Source Colors
Before generating anything, decide how many distinct color families your system needs. A typical web application requires:
- One primary color (your brand color) - the color used for primary interactive elements, links, and key UI components
- One neutral color (usually gray or a slightly tinted gray) - used for backgrounds, borders, text, and structural elements
- Two or three semantic colors - green for success, red for error, amber or yellow for warning
Keep this list short. Every additional color family you add requires a full tonal scale, a semantic mapping layer, and maintenance overhead going forward. Start with primary and neutral, add semantic colors only for states you know you need.
Step 2: Generate the Tonal Scale
A tonal scale for each color family should cover the full range from near-white to near-black in evenly spaced steps. Tailwind's color system uses 11 steps (50, 100, 200 through 900), and this convention has become a useful standard because it leaves room for nuance without becoming unmanageable.
The EvvyTools design token generator automates this step. Enter a brand color in any format (HEX, RGB, or HSL), and it generates the full 10-step tonal scale with values in all three formats, plus a WCAG contrast ratio for text against white and text against black at each step. This contrast information is critical for deciding which palette steps can carry readable text.
// Example: generating a tonal scale for brand blue programmatically
// Using the HSL model - hold hue and saturation, vary lightness
const generateScale = (hue, saturation) => {
const steps = [95, 88, 80, 70, 60, 50, 40, 30, 20, 12];
return steps.map((lightness, index) => ({
step: index === 0 ? 50 : index * 100,
value: `hsl(${hue}, ${saturation}%, ${lightness}%)`,
}));
};
const brandBlueScale = generateScale(220, 90);
// Returns steps 50 through 900 in HSL notation
Step 3: Map to Semantic Tokens
Once you have your palette, the semantic mapping layer is where you make decisions about purpose. This is not mechanical - it requires judgment about how your interface works. Some standard semantic token categories:
- Surface: backgrounds for pages, cards, modals, and overlays
- Text: heading, body, caption, disabled, and on-brand-color variants
- Border: default, focused, error, and divider variants
- Interactive: default, hover, active, disabled, and focus-ring variants
- Feedback: success, warning, error, and info variants
Each semantic token should map to exactly one palette step for each color mode (light and dark). Avoid semantic tokens that reference raw hex values directly. The whole point is to have a single lookup table that changes when the mode changes.
/* Light mode semantic tokens */
:root {
--color-surface-page: var(--color-neutral-50);
--color-surface-card: var(--color-white);
--color-text-heading: var(--color-neutral-900);
--color-text-body: var(--color-neutral-700);
--color-interactive-default: var(--color-blue-500);
--color-interactive-hover: var(--color-blue-600);
--color-border-default: var(--color-neutral-200);
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
:root {
--color-surface-page: var(--color-neutral-950);
--color-surface-card: var(--color-neutral-900);
--color-text-heading: var(--color-neutral-50);
--color-text-body: var(--color-neutral-300);
--color-interactive-default: var(--color-blue-400);
--color-interactive-hover: var(--color-blue-300);
--color-border-default: var(--color-neutral-700);
}
}
Step 4: Export to Your Toolchain
Design token systems need to exist in multiple formats: CSS custom properties for runtime use, Tailwind configuration for utility-class-based projects, SCSS variables for teams still on preprocessors, and W3C Design Token JSON for design tool integration.
The generator handles this. Once your palette and semantic layer are defined, you can export in any of those formats without retyping values. For Tailwind specifically:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: 'hsl(220, 90%, 95%)',
100: 'hsl(220, 90%, 88%)',
200: 'hsl(220, 90%, 80%)',
300: 'hsl(220, 90%, 70%)',
400: 'hsl(220, 90%, 60%)',
500: 'hsl(220, 90%, 50%)',
600: 'hsl(220, 90%, 40%)',
700: 'hsl(220, 90%, 30%)',
800: 'hsl(220, 90%, 20%)',
900: 'hsl(220, 90%, 12%)',
},
},
},
},
};
The Tailwind documentation on customizing colors explains how the extend key works versus replacing the default palette entirely. The Style Dictionary library on GitHub is the most mature open-source tool for managing multi-format token exports if you need to go beyond what a browser-based tool provides.
Photo by Nemuel Sereti on Pexels
Common Mistakes in Token System Design
Naming tokens by appearance rather than purpose. A token called --color-light-blue breaks when the design updates to a different shade of blue or when dark mode makes that shade dark. Name tokens by what they do: --color-interactive-default.
Skipping the semantic layer entirely. Some teams define a palette and then use palette tokens directly in component styles. This works until you add dark mode or try to rebrand. Without a semantic layer, every component becomes a manual update.
Creating too many one-off tokens. If a component needs a color that does not exist in your semantic layer, the correct response is to decide whether the use case warrants a new semantic token, not to hardcode a hex value. Resist the temptation to add tokens for single components.
Not checking contrast at palette creation time. The WebAIM contrast checker and WCAG 2.1 contrast requirements define minimum ratios for accessible text. If you discover contrast failures after building components, you will need to shift your entire palette, which affects every decision downstream.
"The token naming convention is the hardest decision in the whole system and also the one teams spend the least time on. If you get the names wrong, refactoring is a find-and-replace problem across every component in your codebase." - Dennis Traina, 137Foundry
Resources and Further Reading
- CSS Custom Properties - MDN - comprehensive reference for how CSS variables work in browsers
- Design Tokens Community Group specification - the evolving standard for cross-tool token formats
- Style Dictionary on GitHub - open-source library for transforming and exporting design tokens
- Tailwind color customization - how to integrate a custom palette with the Tailwind utility system
- WCAG 2.1 Understanding Color Contrast - the specification behind contrast ratio requirements
- CSS Colors Level 4 specification - the full color model spec including newer color spaces
If you want to understand the underlying color math before generating anything, the companion guide on building a web color system using HEX, RGB, and HSL covers how the three formats relate to each other and why HSL is the right format for palette generation.
A token system is only as good as the naming decisions that shape it. The tooling can generate the values, check the contrast, and export to any format your project needs. The work that matters is deciding what each token means, keeping that list small enough to maintain, and enforcing consistency through code review rather than hoping everyone remembers the rules.

