Deep Dive: Core vs Semantic Tokens
The Stable Contract
Design tokens should be a stable contract between the design layer and the structure layer in a design system. When a system inevitably goes through a rebrand, we want the surface layer to be quick to adopt and change, while maintenance of those changes requires minimal effort.
We achieve this through layered abstraction—encoding design decisions into the structure of our components while offering small escape hatches for maintainers to easily theme or swap underlying styles. The question isn't whether to use layers, but at what depth do you want to make changes?
The Tree Model of Abstraction
Think of tokens as a tree structure. At the root, you have raw values—the actual hex codes, pixel values, and timing functions. As you move toward the leaves (the components), each branch adds a layer of abstraction and meaning.
Depth 0: Raw Value → #fafafa
↓
Depth 1: Primitive → neutral-100
↓
Depth 2: Semantic → surface-primary (or background-neutral-primary)
↓
Depth 3: Component → card-background-default
↓
Depth 4+: Variant → card-background-hoverIf you have a change that affects many items, would you rather chase down 700+ uses of the same value at the component level, or change one value further up the tree and let it cascade down the branches?
Depth Trade-offs
| Depth | Abstraction Level | Change Impact |
|---|---|---|
| 0 | Raw value | Changes everything using that value |
| 1 | Primitive | Changes all semantics referencing it |
| 2 | Semantic | Changes all components using that role |
| 3 | Component | Changes one component's appearance |
| 4+ | Variant/State | Changes one specific state |
A shallow depth of abstraction forces you to either make tokens support more layers (losing fidelity) or broaden the number of tokens you expose to consumers. Neither is ideal.
The Core Layer: Raw Building Blocks
Core tokens define raw values with no inherent meaning. A color like core.color.palette.neutral.600 is just a gray—it doesn't say “use me for text” or “use me for borders.” This intentional meaninglessness is what makes core tokens stable and reusable.
What Lives in Core
| Category | Path | Examples |
|---|---|---|
| Color Palettes | core.color.palette.* | neutral.100-800, red.100-800, blue.100-800 |
| Mode Colors | core.color.mode.* | light, dark, black, white, transparent |
| Spacing Scale | core.spacing.size.* | 00 (0), 01 (1px), 02 (2px), ... 10 (64px) |
| Typography Ramp | core.typography.ramp.* | 1 (10px), 2 (12px), ... 16 (192px) |
| Font Weights | core.typography.weight.* | thin (100), regular (400), bold (700) |
| Motion | core.motion.* | easing-smooth, easing-snappy, duration-short |
| Border Radii | core.shape.radius.* | none (0), 01 (2px), 02 (4px), full (9999px) |
| Elevation | core.elevation.* | level.1, level.2, depth.0-4 |
Core Token Structure
// core/color.tokens.json
{
"palette": {
"neutral": {
"600": {
"$type": "color",
"$value": "#555555",
"$description": "Level 600 of the neutral color scale, a dark shade."
}
}
}
}Notice that descriptions are purely factual—“a dark shade” rather than “use for secondary text.” Core tokens describe what they are, not how to use them.
The Semantic Layer: Purpose and Meaning
Semantic tokens assign roles by referencing other tokens. They answer the question: “What is this value for?” This is where design decisions are encoded—where we say “primary text should be dark in light mode and light in dark mode.”
Semantic Tokens Can Reference Other Semantics
A key insight: semantic tokens aren't limited to referencing only core tokens. They can reference other semantic tokens when it makes sense for the abstraction. This is how you build useful layers of meaning:
// Semantic referencing core (base role)
{
"status": {
"danger": {
"$type": "color",
"$value": "{core.color.palette.red.500}",
"$description": "Base danger/error color"
}
}
}
// Semantic referencing semantic (derived role)
{
"components": {
"button": {
"danger": {
"background": {
"$type": "color",
"$value": "{semantic.color.status.danger}",
"$description": "Danger button inherits from status.danger"
}
}
},
"alert": {
"danger": {
"border": {
"$type": "color",
"$value": "{semantic.color.status.danger}",
"$description": "Alert border also inherits from status.danger"
}
}
}
}
}Now if you change status.danger, both the button and alert update together. This is the power of choosing the right depth of abstraction.
What Lives in Semantic
| Category | Path | Purpose |
|---|---|---|
| Foreground | semantic.color.foreground.* | Text, icons, and other content colors |
| Background | semantic.color.background.* | Surface and container colors |
| Border | semantic.color.border.* | Dividers, outlines, and boundaries |
| Status | semantic.color.status.* | Info, success, warning, danger indicators |
| Interaction | semantic.interaction.* | Hover, active, disabled, selected states |
| Components | semantic.components.* | Button, input, badge, and other UI elements |
The Reference Chain
References form a directed graph from components down to raw values. The depth you choose determines how changes cascade:
Component CSS
↓ uses
semantic.components.button.danger.background
↓ references
semantic.color.status.danger
↓ references
core.color.palette.red.500
↓ contains
"#d9292b" (the actual value)Changing core.color.palette.red.500 updates everything that references it. Changing semantic.color.status.danger updates all danger-related components but leaves other uses of red unchanged. Changing semantic.components.button.danger.background only affects that specific button variant.
Practical Example: The Rebrand Scenario
| Raw Value | Primitive | Semantic | Component |
|---|---|---|---|
| 2px | unit-2 | grid-gap-xs | table-margin-compact |
| 8px | unit-5 | grid-gap-md | table-margin-default |
| #fafafa | neutral-100 | surface-primary | card-background-default |
| #fee197 | yellow-100 | background-feedback-warning | alert-warning-background |
| #2a2a2a | neutral-900 | foreground-primary | foreground-body-primary |
| cubic-bezier(...) | easing-snappy | destructive-action-easing | chip-remove-action-easing |
Mode-Aware Tokens
Rather than duplicating token files for light/dark themes, we use $extensions.design.paths to encode both variants in a single definition:
{
"foreground": {
"primary": {
"$type": "color",
"$value": "{core.color.mode.dark}",
"$extensions": {
"design.paths.light": "{core.color.mode.dark}",
"design.paths.dark": "{core.color.mode.light}"
},
"$description": "Primary foreground color for text and icons"
}
}
}/* Generated CSS */
:root {
--semantic-color-foreground-primary: #141414;
}
[data-theme="dark"] {
--semantic-color-foreground-primary: #fafafa;
}Pitfalls to Avoid
1. Raw Values in Semantic Tokens
Semantic tokens should never contain raw values. This breaks the reference chain and prevents systematic updates.
// BAD: Raw value in semantic token
{
"foreground": {
"primary": {
"$type": "color",
"$value": "#141414" // Breaks the reference chain!
}
}
}
// GOOD: Reference to core token
{
"foreground": {
"primary": {
"$type": "color",
"$value": "{core.color.mode.dark}"
}
}
}2. Circular References
While semantic-to-semantic references are valid, they cannot form cycles. The validator catches these at build time.
// BAD: Circular reference
{
"accent": { "$value": "{semantic.color.link}" },
"link": { "$value": "{semantic.color.accent}" } // Cycle!
}
// GOOD: Linear chain
{
"accent": { "$value": "{core.color.palette.red.500}" },
"link": { "$value": "{semantic.color.accent}" } // Derives from accent
}3. Using Core Tokens Directly in Components
Components should use semantic tokens, not core tokens. This ensures theming works correctly and design decisions stay centralized.
// BAD: Component using core token
.button {
background: var(--core-color-palette-blue-500);
}
// GOOD: Component using semantic token
.button {
background: var(--semantic-color-background-brand);
}4. Wrong Depth of Abstraction
Choose the depth that matches how you expect changes to cascade. Too shallow means more manual updates; too deep means less flexibility.
// BAD: Too shallow - button directly references core
{
"button": {
"danger": {
"background": { "$value": "{core.color.palette.red.500}" }
}
},
"alert": {
"danger": {
"border": { "$value": "{core.color.palette.red.500}" }
}
}
}
// Changing "danger" color requires updating both tokens
// GOOD: Appropriate depth - shared semantic role
{
"status": {
"danger": { "$value": "{core.color.palette.red.500}" }
},
"button": {
"danger": {
"background": { "$value": "{semantic.color.status.danger}" }
}
},
"alert": {
"danger": {
"border": { "$value": "{semantic.color.status.danger}" }
}
}
}
// Changing "danger" color requires updating one tokenThe Restaurant Menu Problem
For a system to be quick to understand, it's important to reduce mental decisions as early as possible. It should be obvious what to grab off the bat, rather than overwhelming someone with fine-grained abstraction.
Finding the balance between delicate detail and “too simple to be useful” can be difficult. The details become apparent after understanding how your consuming audience turns to you for help with their needs.
Summary
- Tokens form a tree with raw values at the root and components at the leaves.
- Depth determines change impact—choose the level where you want changes to cascade.
- Core tokens are raw values with no inherent meaning—the building blocks.
- Semantic tokens assign purpose and can reference both core and other semantic tokens.
- Avoid raw values in semantic tokens, circular references, and using core tokens directly in components.
- Balance abstraction—too shallow means more manual updates; too deep means less flexibility.
The discipline of choosing the right depth of abstraction is what makes the system scalable. When you need to rebrand, support a new theme, or audit accessibility, well-structured tokens ensure you can make changes confidently at the appropriate level.
Next Steps
Understanding the layered model is foundational. From here, explore how tokens are named and organized, how multi-brand theming works, and how the build pipeline transforms tokens into CSS, SCSS, and TypeScript.
Source files:
ui/designTokens/core/— Modular core token filesui/designTokens/semantic/— Modular semantic token filesui/designTokens/designTokens.json— Composed aggregate