Color Token Patterns
The Problem
Tailwind CSS ships with approximately 22 color families, each with 11 shades (50 through 950), resulting in 240+ color utilities. In practice, teams end up using blue-500 in one component, blue-600 in another, and indigo-500 in a third — all intended to mean "primary button blue." Without constraints, every shade is equally valid, so inconsistency grows silently.
The same drift happens with grays. One developer uses gray-100 for a card background, another picks slate-50, and a third reaches for zinc-200. All are "light backgrounds," but none match. Over time the UI develops a patchwork of subtly different tones that undermines visual coherence.
The Solution
Reset all default colors and define a small set of semantic color tokens organized by purpose. After the reset, bg-blue-500 and text-gray-700 no longer work — the team is forced to use the project's intentional color vocabulary.
Token Categories
The categories below follow a semantic layering approach — raw palette values are replaced by role-based tokens that components reference. This is the same principle as the Three-Tier Color Strategy (palette → theme → component), applied specifically to Tailwind's @theme system.
Organize colors into five groups:
- Brand colors —
primary,secondary,accent, each withlight,base, anddarkvariants - Semantic/state colors —
success,warning,error,infofor feedback and status - Surface colors —
surface,surface-alt,surface-inversefor backgrounds - Text colors —
text,text-muted,text-inversefor readable hierarchy - Border colors —
border,border-focusfor edges and focus rings
The @theme Color Block
@theme {
/* Reset ALL default colors */
--color-*: initial;
/* ── Brand ── */
--color-primary-light: hsl(217 91% 60%);
--color-primary: hsl(221 83% 53%);
--color-primary-dark: hsl(224 76% 48%);
--color-secondary-light: hsl(250 80% 68%);
--color-secondary: hsl(252 78% 60%);
--color-secondary-dark: hsl(255 70% 52%);
--color-accent-light: hsl(38 95% 64%);
--color-accent: hsl(33 95% 54%);
--color-accent-dark: hsl(28 90% 46%);
/* ── State ── */
--color-success: hsl(142 71% 45%);
--color-warning: hsl(38 92% 50%);
--color-error: hsl(0 84% 60%);
--color-info: hsl(199 89% 48%);
/* ── Surface ── */
--color-surface: hsl(0 0% 100%);
--color-surface-alt: hsl(210 40% 96%);
--color-surface-inverse: hsl(222 47% 11%);
/* ── Text ── */
--color-text: hsl(222 47% 11%);
--color-text-muted: hsl(215 16% 47%);
--color-text-inverse: hsl(210 40% 98%);
/* ── Border ── */
--color-border: hsl(214 32% 91%);
--color-border-focus: hsl(221 83% 53%);
}
After this configuration, Tailwind utilities like bg-surface, text-primary, and border-border-focus are the only color options available. Reaching for bg-gray-100 causes a build error.
Demos
Default Grays vs Semantic Surface Tokens
The left side shows a sampling of Tailwind's 22 default gray shades — all technically valid for backgrounds. The right side shows the 3 semantic surface tokens that replace them. Fewer choices means faster decisions and guaranteed consistency.
Button Set with Semantic Color Tokens
These buttons use only the semantic color tokens — primary, secondary, error, and accent. No numeric color shades are involved. Every button in the project uses these same tokens, so the palette stays consistent.
Card with Surface, Text, and Border Tokens
This card demonstrates all five token categories working together. The background uses surface and surface-alt, text uses text and text-muted, borders use border, and the badge uses primary brand color. Every color in the component maps to exactly one semantic token.
Palette Growth Naming
When a project starts with a tight token set, each color family typically has only one value. Name it plainly — gray, not gray1. If the project later needs a second shade in that family, add gray2. A third becomes gray3.
@theme {
/* ── Initial palette ── */
--color-gray: hsl(25 5% 45%);
/* ── Added later when a dark card surface was needed ── */
--color-gray2: hsl(0 3% 13%);
}
This "no-number-for-first" rule keeps initial token names clean and avoids a renaming cascade when the palette grows:
gray→ the original gray, used since day onegray2→ added later for dark backgroundsgray3→ added even later for a muted border
Compare with numbering from 1 (gray1, gray2, gray3) — it forces the first token to carry a meaningless suffix, and every existing reference needs updating if you decide to retroactively insert gray1.
The pattern applies to every color family: primary / primary2, surface / surface2, accent / accent2, and so on.
Real-World Example
The zmod project uses exactly this pattern:
--zd-color-gray: rgb(120, 113, 108); /* Original gray */
--zd-color-gray2: #201f1f; /* Added later for dark backgrounds */
No renaming was needed when gray2 arrived — the original gray stayed untouched across the entire codebase.
Before and After: Growing a Palette
This demo shows a simple card UI that initially uses a single gray token. When the design later requires a darker card surface, gray2 is added without touching the original gray.
Why Not Number from 1?
Starting with gray1 seems symmetrical, but it creates problems:
- Visual noise on the most common token — The vast majority of references use the first color.
gray1adds a meaningless digit everywhere. - Renaming pressure — If you start with
grayand later need to "organize," you might feel compelled to rename it togray1for consistency. That touches every file. The no-number-for-first rule removes that pressure entirely. - Signals intent —
gray2clearly communicates "this is the second gray, added alongside the original." Withgray1/gray2, both look like they were planned from the start.
When to Use
This color token strategy works best when combined with the spacing token strategy from the parent article. Together, they constrain the two most common sources of visual drift — spacing and color — into a small, intentional design vocabulary.
Apply color tokens when:
- The project has more than one developer making color choices
- The design system specifies named colors (e.g., "primary," "surface") rather than hex values
- The brand has strict color guidelines that must be enforced consistently