The cascading breakage problem
Semantic tokens are a huge improvement over raw values. But they create a new problem: over-coupling. When your primary button, link underline, badge background, focus ring, and chart accent all use var(--color-accent), any change to that token ripples through every component simultaneously.
Sometimes that is exactly what you want. A brand color change should update everything. But sometimes you want to adjust the button's hover state without affecting the focus ring, or darken the badge background without changing the link color. Semantic tokens alone do not give you that granularity.
You suspected this was a problem the first time you changed a global token and something unexpected broke. You were right.
What component tokens look like
Component tokens sit on top of semantic tokens. They reference the semantic layer by default but can be overridden independently.
/* Semantic layer (shared) */
--color-accent: #6366F1;
--color-accent-hover: #4F46E5;
--color-bg: #FFFFFF;
--color-surface: #F4F4F5;
--color-text: #18181B;
--color-border: #E4E4E7;
/* Component layer (targeted) */
--button-bg: var(--color-accent);
--button-bg-hover: var(--color-accent-hover);
--button-text: #FFFFFF;
--button-border: transparent;
--button-ring: var(--color-accent);
--card-bg: var(--color-surface);
--card-border: var(--color-border);
--card-shadow: 0 1px 3px rgba(0,0,0,0.06);
--input-bg: var(--color-bg);
--input-border: var(--color-border);
--input-border-focus: var(--color-accent);
--input-text: var(--color-text);
--input-placeholder: var(--color-muted);By default, --button-bg resolves to the same value as --color-accent. But now you can override --button-bg independently without touching the global accent. The card, input, and every other component are unaffected.
When component tokens are worth the overhead
Component tokens add a layer of indirection. That is not free. Every layer has a maintenance cost. Here is when the cost is justified:
Multiple consumers of the same semantic token
If --color-accent is used by buttons, links, badges, and focus rings, component tokens let you decouple them. Each consumer gets its own override point.
Complex interactive states
Buttons need hover, active, disabled, and loading states. Each state may need different values for background, text, and border. Encoding all of this in semantic tokens pollutes the semantic layer with component-specific concerns.
/* Button state tokens */
--button-bg: var(--color-accent);
--button-bg-hover: var(--color-accent-hover);
--button-bg-active: var(--color-accent-hover);
--button-bg-disabled: var(--color-surface);
--button-text-disabled: var(--color-muted);
--button-opacity-disabled: 0.6;Variant-heavy components
A button with primary, secondary, outline, and ghost variants. Each variant has different colors but the same structure. Component tokens make variants a token swap rather than a style rewrite.
/* Primary button */
.btn-primary {
--button-bg: var(--color-accent);
--button-text: white;
--button-border: transparent;
}
/* Outline button */
.btn-outline {
--button-bg: transparent;
--button-text: var(--color-accent);
--button-border: var(--color-accent);
}
/* Ghost button */
.btn-ghost {
--button-bg: transparent;
--button-text: var(--color-text);
--button-border: transparent;
}
/* All variants share the same template */
.btn {
background: var(--button-bg);
color: var(--button-text);
border: 1px solid var(--button-border);
}This pattern is how shadcn/ui handles button variants under the hood. The visual template is constant. Only the token values change per variant. For the full picture on how shadcn/ui theming works, see customizing shadcn/ui globals.css.
The naming convention
Component token names follow a consistent pattern: --{component}-{property}, optionally with a state or variant suffix.
/* Pattern: --component-property[-state] */
/* Button */
--button-bg
--button-bg-hover
--button-bg-disabled
--button-text
--button-border
--button-ring
/* Card */
--card-bg
--card-border
--card-shadow
--card-radius
/* Input */
--input-bg
--input-border
--input-border-focus
--input-text
--input-placeholder
--input-ring
/* Badge */
--badge-bg
--badge-text
--badge-borderWhen to skip component tokens
Not every project needs the third layer. Two layers (primitives + semantic) are sufficient when:
Small team or solo developer. If one person controls all the components, the overhead of an extra layer may not pay off. You already know which components use which tokens.
Simple component library. If your UI has buttons, cards, and inputs with one variant each, semantic tokens cover it. Component tokens become valuable when you have multiple variants with complex state.
Early-stage product. Ship first, optimize later. You can always add component tokens later by wrapping existing semantic references. The refactor is mechanical.
Component tokens in practice
Here is a complete example showing how a card component uses all three token layers:
/* Layer 1: Primitives */
--zinc-100: #F4F4F5;
--zinc-200: #E4E4E7;
/* Layer 2: Semantic */
--color-surface: var(--zinc-100);
--color-border: var(--zinc-200);
/* Layer 3: Component */
--card-bg: var(--color-surface);
--card-border: var(--color-border);
--card-radius: var(--radius-lg);
--card-padding: var(--space-6);
--card-shadow: var(--shadow-sm);
/* Component styles reference ONLY component tokens */
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
padding: var(--card-padding);
box-shadow: var(--card-shadow);
}Now you can override any card property without touching the semantic layer. Need a card with no shadow? Set --card-shadow: none. Need a card with a colored border? Set --card-border: var(--color-accent). The override is scoped to the card and nothing else changes.
For the complete guide to building a token system from the ground up, read about CSS custom properties explained. To understand which variables every product needs at the semantic layer, see design variables that actually matter.
Component tokens are the third layer that most projects skip. When your system is small, you do not miss them. When your system scales and a single token change breaks three components, you wish you had them. The overhead is minimal: define --button-bg: var(--color-accent) once. The insurance is enormous: every component can be adjusted independently without side effects. Start adding component tokens to your highest-traffic components first, and expand the layer as your system grows.