A rebrand should be a config change: swap a handful of token values, ship, done. In practice it turns into a multi-week hunt through every component for a hardcoded #1a73e8 or a border-radius: 4px that someone typed directly because "it was faster than looking up the token." The difference between those two outcomes is entirely architectural, decided months before the rebrand request ever lands.
The Core Problem: Values vs. Intent
CSS that survives a rebrand never encodes a value directly in a component — it encodes intent, and resolves that intent to a value through a layer of indirection. Compare:
/* Breaks on rebrand: value is hardcoded in the component */
.button-primary {
background: #1a73e8;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
/* Survives rebrand: component expresses intent, token layer resolves it */
.button-primary {
background: var(--color-action-primary);
border-radius: var(--radius-control);
box-shadow: var(--shadow-elevation-1);
}
The second version doesn't know or care what the primary action color actually is. A rebrand becomes a single edit to where --color-action-primary is defined, and every component that referenced it updates automatically. This is the entire trick — everything else in this article is about making that indirection layer complete enough that nothing slips through as a hardcoded value.
Two-Tier Token Structure
Tier 1: Primitives
Raw, meaningless-on-their-own values — a color ramp, a spacing scale, a type scale:
:root {
--blue-500: #1a73e8;
--blue-600: #1557b0;
--gray-100: #f1f3f4;
--gray-900: #202124;
--radius-sm: 4px;
--radius-md: 8px;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-4: 1rem;
}
Primitives change during a rebrand too, but rarely — a new brand usually keeps the same shape of scale (same number of steps, same spacing rhythm) and swaps the actual color ramp.
Tier 2: Semantic Tokens
Purpose-based names that reference primitives, and are what components actually consume:
:root {
--color-action-primary: var(--blue-500);
--color-action-primary-hover: var(--blue-600);
--color-surface: var(--gray-100);
--color-text-default: var(--gray-900);
--radius-control: var(--radius-sm);
}
This layer is where a rebrand actually happens. If the new brand's primary action color shifts from blue to a brand purple, you edit --color-action-primary in one place and every button, link, and focus ring downstream updates without touching a single component file. The rule that keeps this working is strict: components reference semantic tokens only, never primitives directly. The moment a component does background: var(--blue-500) instead of background: var(--color-action-primary), you've reintroduced a hardcoded dependency that a rebrand will miss.
Theming Without Duplicating Component CSS
Custom properties cascade, which means a rebrand — or a dark mode, or a white-label variant — can be scoped without touching component selectors at all:
[data-theme="brand-legacy"] {
--color-action-primary: #1a73e8;
}
[data-theme="brand-2026"] {
--color-action-primary: #6b3fd1;
}
Swap the data-theme attribute on <html> and every component using the semantic token repaints. This is also how you support a transition period where old and new brand need to coexist — a staged rollout, an A/B test, or legacy customer accounts on the old look — without maintaining two copies of your component CSS. See MDN's custom properties guide for cascading and inheritance behavior.
Where This Breaks in Practice
Inline Styles and JS-Computed Values
style={{ background: '#1a73e8' }} in a React component bypasses the cascade entirely and won't be caught by a grep for token names if someone wrote the hex directly. Any place a value is computed in JavaScript rather than read from CSS needs the same discipline — pull from a shared token object (ideally generated from the same source as your CSS tokens) rather than a local literal.
Third-Party and Vendor CSS
Embedded widgets, PDF generators, and email templates often can't consume CSS custom properties at all (email clients strip them). These need their own generation step from the same token source — treat them as a separate build target of the token system, not an exception to it.
Box Shadows and Gradients Get Hardcoded More Than Colors
Colors get tokenized early because they're the obvious rebrand lever, but shadows and gradients are frequently typed by hand per-component because they feel like "one-off polish" rather than brand identity. They're not — a rebrand almost always changes elevation style and often the gradient direction/stops too. Tokenize them the same way:
:root {
--shadow-elevation-1: 0 2px 4px rgba(0, 0, 0, 0.15);
--gradient-hero: linear-gradient(135deg, var(--color-brand-start), var(--color-brand-end));
}
When building these values, the Box Shadow Generator and Gradient Generator are useful for producing the new tier-1 values quickly during the actual rebrand — generate the new shadow/gradient, drop it into the primitive layer, and every component referencing the semantic token updates without a manual pass through the codebase.
Decision Point: How Much Tokenization Is Worth It
| Project size | Recommended token depth |
|---|---|
| Small marketing site, unlikely to rebrand | Primitives only, or even just CSS variables without formal tiers — full system is overkill |
| Product with an established design system | Two-tier (primitive + semantic), enforced via lint rule against hardcoded hex/px in components |
| Multi-brand or white-label platform | Two-tier plus a themeable data-theme layer, tokens generated from a shared JSON source |
| One-off prototype | Skip tokens entirely, optimize for shipping speed instead |
The lint rule matters more than the architecture diagram. A stylelint rule (or a small custom script) that flags raw hex colors, raw pixel border-radius/box-shadow values, or literal colors in JS outside the token file is what actually prevents drift over the months between rebrands — without it, the two-tier system decays back into hardcoded values one "quick fix" at a time.