Three-Tier Color Strategy
The Problem
When building a website or web app, it's tempting to use color values directly in component CSS — picking a hex code for a button, a different shade for a sidebar, and yet another for hover states. This works initially but creates two serious problems:
- Changing the brand color requires hunting through every component — there's no single place to update
- No semantic meaning — seeing
#89b4fain a button tells you nothing about why that color was chosen or what role it plays
A common first improvement is defining a palette of CSS custom properties (--color-blue-500, --color-red-400). But even with a palette, components end up tightly coupled to specific palette colors. If the design system decides that "primary interactive elements" should shift from blue to indigo, you're back to searching every component.
The Solution
Organize colors into three tiers, each with a clear purpose:
| Tier | Name | Purpose | Example |
|---|---|---|---|
| 1 | Palette | Raw color values — the full set of available colors | --palette-blue-500 |
| 2 | Theme | Semantic roles — what each color means in the design | --theme-fg, --theme-accent |
| 3 | Component | Scoped overrides — colors specific to one component | --button-shadow, --card-highlight |
The key insight: each tier only references the tier above it. Components use theme tokens. Theme tokens point to palette colors. Palette holds the actual values.
Code Examples
Tier 1: The Palette
The palette is the raw material — every color available in the system. These are not used directly in components. Think of them as paint tubes: you have them ready, but you don't squeeze them onto the canvas without a plan.
Tier 2: The Theme
Theme tokens give semantic meaning to palette colors. Instead of "blue-500", components see "accent color" or "foreground color". This is the layer that makes redesigns painless — change --theme-accent from blue to indigo in one place, and every component updates.
Tier 3: Component-Scoped Colors
Sometimes a component needs colors that don't fit into the global theme — a shadow, a product variant color, or a subtle gradient stop. These are Tier 3 variables: narrowly scoped, defined on the component itself, and referencing either theme or palette tokens.
All Three Tiers Working Together
Here's a complete example showing how the three tiers connect. Notice how easy it is to trace any color back to its source: component uses theme, theme points to palette.
The Power of Tier 2: Swapping Themes
The biggest advantage of the three-tier system is visible when you change the theme. By remapping Tier 2 pointers, every component updates without touching component CSS.
Complete CSS Code Structure
Here's how a real project would organize the three tiers across files:
/* ===== tokens/palette.css — Tier 1 ===== */
:root {
--palette-blue-100: oklch(92% 0.06 250);
--palette-blue-300: oklch(74% 0.14 250);
--palette-blue-500: oklch(58% 0.2 250);
--palette-blue-700: oklch(42% 0.16 250);
--palette-blue-900: oklch(28% 0.1 250);
--palette-red-500: oklch(58% 0.22 25);
--palette-green-500: oklch(58% 0.18 150);
--palette-amber-500: oklch(62% 0.18 65);
--palette-gray-50: oklch(98% 0.003 264);
--palette-gray-100: oklch(96% 0.005 264);
--palette-gray-300: oklch(82% 0.01 264);
--palette-gray-500: oklch(58% 0.01 264);
--palette-gray-700: oklch(40% 0.015 264);
--palette-gray-900: oklch(22% 0.015 264);
}
/* ===== tokens/theme.css — Tier 2 ===== */
:root {
/* Layout */
--theme-fg: var(--palette-gray-900);
--theme-bg: var(--palette-gray-50);
--theme-surface: oklch(100% 0 0);
--theme-border: var(--palette-gray-300);
--theme-muted: var(--palette-gray-500);
/* Interactive */
--theme-accent: var(--palette-blue-500);
--theme-accent-hover: var(--palette-blue-700);
--theme-accent-subtle: var(--palette-blue-100);
--theme-accent-fg: oklch(98% 0.01 250);
/* Feedback */
--theme-error: var(--palette-red-500);
--theme-success: var(--palette-green-500);
--theme-warning: var(--palette-amber-500);
}
/* ===== components/button.css — Tier 3 ===== */
.btn {
/* Component-specific color derived from theme.
Relative color syntax (oklch(from ...)) — Baseline 2024.
For wider support, use a hardcoded fallback. */
--btn-shadow: oklch(from var(--theme-accent) l c h / 0.3);
background: var(--theme-accent);
color: var(--theme-accent-fg);
box-shadow: 0 2px 8px var(--btn-shadow);
}
.btn:hover {
background: var(--theme-accent-hover);
}
/* ===== components/product-card.css — Tier 3 ===== */
.product-card {
/* Colors unique to this component, not in theme */
--card-variant: var(--palette-blue-500);
--card-glow: oklch(from var(--card-variant) l c h / 0.15);
}
.product-card--sunset {
--card-variant: oklch(62% 0.2 50);
}
Tailwind CSS: Three Tiers with Custom Theme Config
The three-tier strategy maps naturally to Tailwind's configuration. Palette colors go in theme.colors, theme tokens become CSS custom properties that Tailwind classes reference.
Tailwind: Theme Switching with Tier 2
The same Tailwind markup works with completely different color schemes — just change the CSS variables.
Dark Mode: Just Another Tier 2 Mapping
Dark mode fits naturally into the three-tier system. The palette (Tier 1) stays the same — you already have all the colors. Dark mode is simply an alternative Tier 2 mapping that picks different palette values for the same semantic tokens. The CSS light-dark() function makes this especially clean by co-locating both values in a single declaration.
For a deep dive into dark mode techniques, see Dark Mode Strategies.
With light-dark(), the same Tier 2 mapping can be expressed without separate selectors:
/* Tier 2 with light-dark() — both modes in a single declaration */
:root {
color-scheme: light dark;
--theme-bg: light-dark(var(--palette-gray-50), oklch(15% 0.01 264));
--theme-surface: light-dark(oklch(100% 0 0), oklch(20% 0.015 264));
--theme-fg: light-dark(var(--palette-gray-900), oklch(92% 0.005 264));
--theme-muted: light-dark(var(--palette-gray-500), oklch(60% 0.01 264));
--theme-border: light-dark(var(--palette-gray-300), oklch(30% 0.015 264));
--theme-accent: light-dark(var(--palette-blue-500), oklch(70% 0.17 250));
--theme-accent-fg: light-dark(oklch(98% 0.01 250), oklch(15% 0.01 250));
}
Tier 1 and Tier 3 stay exactly the same — only Tier 2 changes. Components never know whether they're in light or dark mode.
Common AI Mistakes
- Skipping Tier 2 — using palette colors directly in components (
color: var(--palette-blue-500)) defeats the purpose; when the brand changes, every component must be updated - Too many Tier 3 variables — if a component defines 10+ local color variables, it's likely reinventing the theme layer; promote those to Tier 2
- Not separating palette from theme — defining
--primary: oklch(58% 0.2 250)mixes raw values with semantic meaning, making it impossible to swap palettes - Inconsistent naming — mixing
--color-primary,--brand-blue, and--accentat the same level creates confusion; use consistent prefixes per tier (--palette-*,--theme-*) - Making Tier 1 too small — a palette with only 5 colors forces components to invent their own raw values (Tier 3 colors with hardcoded values), breaking the system
- Hardcoding colors in Tailwind utilities — using
bg-blue-500instead ofbg-theme-accentbypasses the theme layer entirely
When to Use
- Any project with more than a few components — the overhead of three tiers pays off as soon as you need consistency
- Multi-theme or white-label products — Tier 2 makes theme switching trivial
- Design system or component library — components should reference theme tokens, not raw colors
- Dark mode — dark mode is just another Tier 2 mapping (see Dark Mode Strategies)
- Gradual adoption — you can start with Tier 1 + 2 and add Tier 3 as components need scoped colors
When three tiers is overkill
- Single-page sites with one brand color and no theme variations
- Quick prototypes where speed matters more than maintainability
- Static sites with minimal interactive elements
Related Articles
- Color Palette Strategy — How to choose and generate palette colors (Tier 1)
- Dark Mode Strategies — Dark mode techniques that work with Tier 2 token swapping
- Advanced Custom Properties — Fallback chains, space toggles, and component APIs that implement the three-tier pattern
- Theming Recipes — Production-ready theme recipes using this architecture
- Color Token Patterns — Applying three-tier tokens in Tailwind's
@themesystem