Canon Design Tokens
The Principle
Tailwind for structure, Canon for aesthetics.
This division isn't arbitrary. It's the Subtractive Triad applied to CSS.
The Problem with Utility-First
Tailwind is powerful. It's also dangerous.
<!-- Tailwind everywhere -->
<div class="bg-slate-900 rounded-xl shadow-lg border border-slate-700 p-6">
<h2 class="text-white text-xl font-semibold mb-4">Card Title</h2>
<p class="text-slate-400 text-sm leading-relaxed">
Some content that goes here with lots of utility classes.
</p>
</div>
What color is slate-900? What's the semantic meaning of rounded-xl? When should you use shadow-lg vs shadow-md?
The design decisions are scattered across every class.
Change the design system and you change every component. The truth (what the design should be) is fragmented across thousands of class applications.
Canon: Semantic Tokens
Canon provides design tokens with semantic meaning:
:root {
/* Background hierarchy */
--color-bg-pure: #000000;
--color-bg-elevated: #0a0a0a;
--color-bg-surface: #111111;
--color-bg-subtle: #1a1a1a;
/* Foreground hierarchy */
--color-fg-primary: #ffffff;
--color-fg-secondary: rgba(255, 255, 255, 0.8);
--color-fg-tertiary: rgba(255, 255, 255, 0.6);
--color-fg-muted: rgba(255, 255, 255, 0.4);
}
The token name tells you what it's for:
--color-bg-surface→ Background for card-like surfaces--color-fg-secondary→ Text that's slightly de-emphasized--color-border-default→ Standard border color
The design decision is in one place. Usage is everywhere.
The Division
Structure: Tailwind
Layout, spacing, flexbox, grid—these are structural decisions:
<div class="flex items-center gap-4 p-6">
<div class="flex-1">
<span class="block mb-2">Title</span>
<span class="block">Subtitle</span>
</div>
</div>
Tailwind's utility classes are perfect here. The structure is explicit in the markup.
Aesthetics: Canon
Colors, radii, shadows, typography—these are design decisions:
<div class="flex items-center gap-4 p-6 card">
<div class="flex-1">
<span class="block mb-2 card-title">Title</span>
<span class="block card-subtitle">Subtitle</span>
</div>
</div>
<style>
.card {
background: var(--color-bg-surface);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);
}
.card-title {
color: var(--color-fg-primary);
font-size: var(--text-body-lg);
}
.card-subtitle {
color: var(--color-fg-tertiary);
font-size: var(--text-body-sm);
}
</style>
Structure in the class, design in the style block.
The Token Categories
Colors
Hierarchical naming for visual weight:
| Token | Value | Use |
|---|---|---|
--color-bg-pure |
#000000 |
Page background |
--color-bg-elevated |
#0a0a0a |
Slightly raised surfaces |
--color-bg-surface |
#111111 |
Cards, modals |
--color-bg-subtle |
#1a1a1a |
Subtle backgrounds |
--color-fg-primary |
#ffffff |
Main content |
--color-fg-secondary |
rgba(255,255,255,0.8) |
Supporting content |
--color-fg-tertiary |
rgba(255,255,255,0.6) |
De-emphasized |
--color-fg-muted |
rgba(255,255,255,0.4) |
Very subtle |
The hierarchy is the design system. You don't need to remember hex codes.
Spacing
Golden ratio (φ = 1.618) creates natural proportions:
| Token | Value | Ratio |
|---|---|---|
--space-xs |
0.5rem |
8px |
--space-sm |
1rem |
16px |
--space-md |
1.618rem |
~26px |
--space-lg |
2.618rem |
~42px |
--space-xl |
4.236rem |
~68px |
<style>
.section {
padding: var(--space-lg);
margin-bottom: var(--space-xl);
}
.card-content {
padding: var(--space-md);
gap: var(--space-sm);
}
</style>
The spacing feels right because it follows natural proportions.
Typography
Fluid scales that respond to viewport:
:root {
--text-display: clamp(2.5rem, 4vw + 1.5rem, 5rem);
--text-h1: clamp(2rem, 3vw + 1rem, 3.5rem);
--text-h2: clamp(1.5rem, 2vw + 0.75rem, 2.25rem);
--text-h3: clamp(1.25rem, 1.5vw + 0.5rem, 1.75rem);
--text-body-lg: 1.125rem;
--text-body: 1rem;
--text-body-sm: 0.875rem;
--text-caption: 0.75rem;
}
No media queries for font sizes. The clamp() function handles responsive scaling.
Radii
Consistent roundness across the system:
| Token | Value | Use |
|---|---|---|
--radius-sm |
6px |
Small elements, inputs |
--radius-md |
8px |
Buttons, badges |
--radius-lg |
12px |
Cards, modals |
--radius-xl |
16px |
Large containers |
--radius-full |
9999px |
Pills, avatars |
<style>
.button {
border-radius: var(--radius-md);
}
.card {
border-radius: var(--radius-lg);
}
.avatar {
border-radius: var(--radius-full);
}
</style>
Animation
Consistent motion:
:root {
--ease-standard: cubic-bezier(0.4, 0.0, 0.2, 1);
--duration-micro: 200ms;
--duration-standard: 300ms;
--duration-complex: 500ms;
}
<style>
.button {
transition: all var(--duration-micro) var(--ease-standard);
}
.modal {
transition: all var(--duration-standard) var(--ease-standard);
}
</style>
Migration Pattern
When you encounter Tailwind design classes, migrate them:
Before (Tailwind design)
<div class="bg-slate-900 rounded-xl shadow-lg border border-slate-700 p-6">
<h2 class="text-white text-xl font-semibold">Title</h2>
<p class="text-slate-400 text-sm">Content</p>
</div>
After (Canon design)
<div class="p-6 card">
<h2 class="card-title">Title</h2>
<p class="card-body">Content</p>
</div>
<style>
.card {
background: var(--color-bg-surface);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
border: 1px solid var(--color-border-default);
}
.card-title {
color: var(--color-fg-primary);
font-size: var(--text-h3);
font-weight: var(--font-semibold);
}
.card-body {
color: var(--color-fg-tertiary);
font-size: var(--text-body-sm);
}
</style>
Tailwind p-6 stays (structure). Design tokens replace color/radius/shadow.
Detection Patterns
These Tailwind classes signal design decisions that should be Canon:
| Pattern | Issue | Canon Replacement |
|---|---|---|
rounded-* |
Visual roundness | var(--radius-*) |
bg-slate-* |
Background color | var(--color-bg-*) |
text-white/80 |
Text opacity | var(--color-fg-secondary) |
shadow-* |
Elevation | var(--shadow-*) |
text-xl |
Type scale | var(--text-h3) |
The Single Source
All tokens live in app.css:
/* app.css */
:root {
/* All Canon tokens defined here */
--color-bg-pure: #000000;
--color-bg-surface: #111111;
/* ... */
}
Change the design system in one file. Every component updates automatically.
This is DRY at the design level.
Why It Works
The division works because:
- Structure rarely changes → Flexbox stays flexbox
- Aesthetics evolve → Colors, radii, typography get refined
- Tailwind excels at structure → Explicit, composable layout
- CSS variables excel at theming → One source, many uses
Each tool does what it does best.
Cross-Property References
Canon Reference: See The CSS Canon Standards for the complete token reference and design rationale.
Research Depth: Read Subtractive Form Design for the theory behind semantic tokens.
Practice: The Praxis exercises on .space use Canon tokens throughout—study the implementations.
See Also
- CSS Canon — Complete token reference with WCAG compliance, motion tokens, and utility class guidelines
- Taste Reference — Human-curated visual references from Are.na that inform Canon decisions
Reflection
Before the praxis:
- Open a component in your project. Which classes are structural (layout)? Which are aesthetic (design)?
- How many places would you need to change if you wanted to adjust your primary color?
- What would it take to support dark/light themes with your current approach?
Praxis: You'll migrate a component from pure Tailwind to the Canon pattern.