Token Override Patterns
Token Override Patterns
Token override patterns provide structured approaches for customizing token values within defined boundaries. Overrides enable themes, brand variations, component customization, and product-specific adjustments while maintaining system coherence. Well-designed override patterns balance flexibility with control, preventing chaos while enabling legitimate customization.
What Are Token Override Patterns
Token override patterns are established mechanisms for replacing or modifying token values in specific contexts. Rather than editing source tokens directly, overrides layer on top, changing values where needed while preserving the base system.
Patterns define how overrides are structured, where they apply, and how conflicts are resolved. Different patterns suit different customization needs.
How Token Override Patterns Work
Layer override pattern stacks token sets:
Base tokens → Theme overrides → Brand overrides → Product overrides
Each layer can override tokens from previous layers:
// Build configuration
module.exports = {
source: [
'tokens/base/**/*.json', // Base tokens
'tokens/dark/**/*.json', // Theme overrides
'tokens/brand-b/**/*.json' // Brand overrides
]
};
Later sources override earlier sources for matching token names.
Scope override pattern limits override applicability:
/* Global token */
:root {
--color-primary: blue;
}
/* Scoped override */
.marketing-section {
--color-primary: green;
}
Overrides apply only within the scoped element.
Variant override pattern creates named variations:
{
"button": {
"background": {
"default": { "$value": "{color.primary}" },
"destructive": { "$value": "{color.error}" }
}
}
}
Components select appropriate variants rather than overriding base values.
Configuration override pattern uses runtime configuration:
const config = {
overrides: {
'color.primary': '#custom-color'
}
};
const tokens = applyOverrides(baseTokens, config.overrides);
Key Considerations
- Override patterns should be documented and sanctioned
- Uncontrolled overrides fragment the design system
- Override scope should match customization need
- Debugging requires understanding active overrides
- Override precedence must be predictable
- Some tokens should not be overridable
- Testing should cover override scenarios
- Performance implications of extensive overrides
Common Questions
How should override boundaries be defined?
Override boundaries establish what can be customized and to what degree.
Token-level boundaries:
{
"color": {
"primary": {
"$value": "#3B82F6",
"$overridable": true
},
"error": {
"$value": "#DC2626",
"$overridable": false // Critical for UX consistency
}
}
}
Category-level boundaries:
- Brand colors: Overridable (product customization)
- Semantic colors: Limited override (maintain meaning)
- Status colors: Not overridable (consistency critical)
- Spacing scale: Not overridable (layout coherence)
Documentation of boundaries:
## Override Policy
### Fully Customizable
- Brand colors (color.brand.*)
- Component borders (component.*.border)
### Customizable with Approval
- Semantic colors (color.semantic.*)
- Typography scale (font.size.*)
### Not Customizable
- Status colors (color.status.*)
- Spacing scale (spacing.*)
Clear boundaries enable customization while protecting system integrity.
How should conflicting overrides be handled?
Conflicts occur when multiple overrides affect the same token.
Explicit precedence rules:
Product overrides > Brand overrides > Theme overrides > Base
The most specific context wins.
Merge strategies for object tokens:
// Shallow merge: Later completely replaces
const result = { ...base, ...override };
// Deep merge: Later supplements
const result = deepMerge(base, override);
Conflict detection:
function detectConflicts(layers) {
const conflicts = [];
const seen = new Map();
layers.forEach(layer => {
Object.keys(layer.tokens).forEach(name => {
if (seen.has(name)) {
conflicts.push({
token: name,
sources: [seen.get(name), layer.name]
});
}
seen.set(name, layer.name);
});
});
return conflicts;
}
Conflict resolution:
- Automated: Apply precedence rules silently
- Warning: Apply rules but log conflicts
- Blocking: Fail build requiring explicit resolution
What about runtime versus build-time overrides?
Build-time overrides compile into final outputs:
// Different builds for different contexts
npm run build:brand-a // Produces brand-a tokens
npm run build:brand-b // Produces brand-b tokens
Advantages: No runtime overhead, simple debugging Disadvantages: Requires separate builds, no dynamic switching
Runtime overrides apply during execution:
// Runtime theme switching
document.documentElement.dataset.theme = 'dark';
:root { --color-bg: white; }
[data-theme="dark"] { --color-bg: black; }
Advantages: Dynamic switching, single build Disadvantages: Runtime overhead, more complex debugging
Hybrid approach uses build-time for structural overrides and runtime for user preferences:
Build: Brand A vs Brand B (different builds)
Runtime: Light vs Dark theme (CSS switching)
Summary
Token override patterns provide structured customization within defined boundaries. Layer patterns stack token sets for brand and theme variations. Scope patterns limit overrides to specific contexts. Variant patterns offer named alternatives. Clear boundaries define what is overridable and to what degree. Conflict resolution follows explicit precedence rules. Build-time versus runtime override selection depends on dynamism requirements and performance considerations.
Buoy scans your codebase for design system inconsistencies before they ship
Detect Design Drift Free