This guide explains the philosophy, architecture, and best practices for building maintainable design systems with tokenctl. For the token format reference (types, fields, expressions, components), see TOKENS.md.
- Philosophy
- Architecture
- Getting Started
- Token Organization
- Responsive Design
- Component Patterns
- Theming
- LLM Integration
- Validation
- Extending Design Systems
- Migration Guide
Large web projects often struggle with CSS maintainability:
- Specificity conflicts requiring cascading overrides
- Inconsistent spacing, colors, and typography
- No single source of truth for design decisions
- Generated code (including LLM output) that drifts from the system
tokenctl uses a tokens-first approach:
- All styling decisions become tokens - Colors, spacing, typography, effects
- Tokens reference other tokens - Building a semantic hierarchy
- Components consume tokens - Never arbitrary values
- Validation enforces rules - Catch violations at build time
This creates a vocabulary that both humans and LLMs can use consistently.
| Principle | What it Means |
|---|---|
| Single Source of Truth | All design values live in token files |
| Semantic Layering | Raw values → semantic names → component usage |
| Reference-Only Components | Components use var(--token), never raw values |
| Validated Architecture | Layer rules enforced via --strict-layers |
| Context-Efficient Manifests | LLMs get exactly the tokens they need |
tokenctl uses a three-layer architecture: Brand (raw values) → Semantic (meaning) → Component (usage). See TOKENS.md for the full diagram, rationale, and layer reference rules.
{
"brand": {
"$layer": "brand",
"$type": "color",
"blue-500": { "$value": "#3b82f6" },
"blue-600": { "$value": "#2563eb" },
"purple-500": { "$value": "#8b5cf6" }
},
"semantic": {
"$layer": "semantic",
"$type": "color",
"primary": { "$value": "{brand.blue-500}" },
"primary-hover": { "$value": "{brand.blue-600}" },
"accent": { "$value": "{brand.purple-500}" }
},
"component": {
"$layer": "component",
"$type": "color",
"btn-bg": { "$value": "{semantic.primary}" },
"btn-bg-hover": { "$value": "{semantic.primary-hover}" }
}
}tokenctl init my-systemtokens/brand/colors.json:
{
"$layer": "brand",
"color": {
"$type": "color",
"blue-500": { "$value": "#3b82f6" },
"blue-600": { "$value": "#2563eb" },
"gray-50": { "$value": "#f9fafb" },
"gray-900": { "$value": "#111827" }
}
}tokens/semantic/colors.json:
{
"$layer": "semantic",
"color": {
"$type": "color",
"primary": {
"$value": "{color.blue-500}",
"$description": "Primary brand color",
"$usage": ["buttons", "links", "focus rings"]
},
"primary-hover": { "$value": "{color.blue-600}" },
"surface": { "$value": "{color.gray-50}" },
"text": { "$value": "{color.gray-900}" }
}
}# Tailwind 4 output
tokenctl build my-system --format=tailwind
# Pure CSS output (no Tailwind dependency)
tokenctl build my-system --format=css
# Validate with layer rules
tokenctl validate my-system --strict-layerstokens/
├── brand/
│ ├── colors.json # Raw color palette
│ ├── spacing.json # Base spacing scale
│ └── typography.json # Font families, weights
├── semantic/
│ ├── colors.json # primary, success, error, etc.
│ ├── spacing.json # spacing-sm, spacing-md, etc.
│ └── typography.json # font-heading, font-body
├── components/
│ ├── button.json # .btn component tokens
│ ├── card.json # .card component tokens
│ └── input.json # Form input tokens
└── themes/
├── light.json # Light theme overrides
└── dark.json # Dark theme (extends light)
Every token can carry metadata for documentation and LLM comprehension: $description, $usage (array of intended uses), $avoid (anti-patterns), $deprecated (migration guidance), and $customizable (safe to override). See Token Structure for the full field reference.
{
"color": {
"primary": {
"$value": "#3b82f6",
"$type": "color",
"$description": "Primary brand color for key actions",
"$usage": ["Primary button backgrounds", "Link text color"],
"$avoid": "Don't use for large background areas",
"$customizable": true
}
}
}Modern responsive design combines two approaches:
- Fluid values - Use
clamp()for smooth scaling - Breakpoint overrides - Discrete changes at specific widths
For continuous scaling, use CSS clamp():
{
"spacing": {
"$type": "dimension",
"section": {
"$value": "clamp(2rem, 5vw, 6rem)",
"$description": "Fluid section padding"
}
},
"font": {
"size": {
"heading": {
"$value": "clamp(1.5rem, 4vw, 3rem)",
"$description": "Fluid heading size"
}
}
}
}These scale smoothly without media queries.
For discrete breakpoint changes, use $responsive:
{
"$breakpoints": {
"sm": "640px",
"md": "768px",
"lg": "1024px",
"xl": "1280px"
},
"spacing": {
"$type": "dimension",
"md": {
"$value": "1rem",
"$responsive": {
"md": "1.25rem",
"lg": "1.5rem"
}
}
},
"font": {
"size": {
"body": {
"$value": "1rem",
"$responsive": {
"md": "1.125rem",
"lg": "1.25rem"
}
}
}
}
}Generated CSS:
:root {
--spacing-md: 1rem;
--font-size-body: 1rem;
}
@media (min-width: 768px) {
:root {
--spacing-md: 1.25rem;
--font-size-body: 1.125rem;
}
}
@media (min-width: 1024px) {
:root {
--spacing-md: 1.5rem;
--font-size-body: 1.25rem;
}
}| Approach | Best For | Example |
|---|---|---|
| Fluid (clamp) | Continuous scaling | Section padding, heading sizes |
| Breakpoint overrides | Discrete changes | Grid columns, layout shifts |
| Both | Complex responsive needs | Combine fluid base with overrides |
Components use $type: "component" and generate CSS classes with base, variants, sizes, and states. See Components for the full schema including states and container queries.
Document component relationships for LLMs:
{
"card": {
"$type": "component",
"$class": "card",
"$description": "Container for card content",
"$contains": ["card-body", "card-title", "card-actions", "card-image"]
},
"card-body": {
"$type": "component",
"$class": "card-body",
"$description": "Main content area inside a card",
"$requires": "card"
},
"card-title": {
"$type": "component",
"$class": "card-title",
"$description": "Title text inside a card",
"$requires": "card"
}
}Manifest output:
{
"components.card": {
"description": "Container for card content",
"contains": ["card-body", "card-title", "card-actions", "card-image"],
"classes": ["card"]
},
"components.card-body": {
"description": "Main content area inside a card",
"requires": "card",
"classes": ["card-body"]
}
}Themes can extend other themes:
// themes/light.json
{
"$description": "Default light theme",
"color": {
"surface": { "$value": "#ffffff" },
"text": { "$value": "#1f2937" }
}
}// themes/dark.json
{
"$extends": "light",
"$description": "Dark theme",
"color": {
"surface": { "$value": "#1f2937" },
"text": { "$value": "#f9fafb" }
}
}Generated CSS uses data-theme attributes:
:root, [data-theme="light"] {
--color-surface: #ffffff;
--color-text: #1f2937;
}
[data-theme="dark"] {
--color-surface: #1f2937;
--color-text: #f9fafb;
}HTML:
<html data-theme="dark">JavaScript:
document.documentElement.setAttribute('data-theme', 'dark');Add $property: true to color tokens to generate CSS @property declarations, enabling smooth animated transitions between themes instead of instant snaps. See CSS @property Declarations for the full specification.
Generate category-specific manifests to minimize LLM context usage:
# All tokens (may be large)
tokenctl build --format=catalog
# Just colors for a color-related task
tokenctl build --format=manifest:color
# Just components for UI work
tokenctl build --format=manifest:components
# Just spacing
tokenctl build --format=manifest:spacingLLMs can search tokens without loading entire files:
# Find tokens by name
tokenctl search "primary"
# Filter by type
tokenctl search --type=color
# Filter by category
tokenctl search --category=spacingExample output:
color.primary: #3b82f6
Primary brand color for key actions
Usage: Primary button backgrounds, Link text color
color.primary-hover: #2563eb
Darker primary for hover states
Manifests include rich metadata for LLM comprehension:
{
"meta": {
"version": "2.1",
"category": "color",
"tokenctl_version": "1.2.0"
},
"tokens": {
"color.primary": {
"value": "#3b82f6",
"type": "color",
"description": "Primary brand color",
"usage": ["buttons", "links", "focus rings"],
"avoid": "Don't use for large backgrounds"
}
}
}Component manifests include composition metadata:
{
"components": {
"card": {
"description": "Container for card content",
"contains": ["card-body", "card-title", "card-actions"],
"classes": ["card"]
}
}
}This tells LLMs which components can be nested together.
tokenctl validate ./my-tokensChecks:
- Token syntax
- Reference resolution (no broken references)
- Type validation (colors are valid colors, etc.)
- Cycle detection (no circular references)
tokenctl validate ./my-tokens --strict-layersEnforces the layer hierarchy:
- Brand layer: Can only contain raw values
- Semantic layer: Can reference brand tokens
- Component layer: Can only reference semantic tokens
Violation example:
[Error] component.btn-bg [component] cannot reference brand.blue-500 [brand]: layer violation
Fix by routing through semantic layer:
{
"semantic": {
"primary": { "$value": "{brand.blue-500}" }
},
"component": {
"btn-bg": { "$value": "{semantic.primary}" }
}
}Dimension and number tokens support $min/$max bounds checking. See Constraints for details.
- Inventory existing values - List all colors, spacing values, font sizes
- Create brand tokens - Raw values only
- Create semantic layer - Map brand to purpose
- Update components - Replace values with
var(--token) - Validate - Run
tokenctl validate --strict-layers
Before:
.btn {
background: #3b82f6;
padding: 0.5rem 1rem;
}After:
.btn {
background: var(--component-btn-bg);
padding: var(--spacing-sm) var(--spacing-md);
}Tailwind 3 uses tailwind.config.js. Migrate to token files:
tailwind.config.js (before):
module.exports = {
theme: {
colors: {
primary: '#3b82f6',
}
}
}tokens/semantic/colors.json (after):
{
"$layer": "semantic",
"color": {
"$type": "color",
"primary": { "$value": "#3b82f6" }
}
}Then: tokenctl build --format=tailwind
Many design tools export W3C tokens. Import directly:
# Figma Tokens export
cp figma-export.json tokens/brand/colors.json
# Add layer annotations
# Add $layer field to each token groupPackaged design systems can be extended using CSS @layer without needing a build step for simple overrides.
Design systems built with tokenctl use this layer order:
@layer tokens, components, themes, user;Later layers automatically override earlier ones—no !important needed.
Just import the base system:
<link rel="stylesheet" href="@acme/design-system/dist/base.css">Create a simple CSS file with your brand values:
my-brand.css:
@layer user {
:root {
--color-primary: #10b981; /* Your brand green */
--color-secondary: #6366f1; /* Your brand purple */
--font-family-base: "Outfit", sans-serif;
}
}<link rel="stylesheet" href="@acme/design-system/dist/base.css">
<link rel="stylesheet" href="my-brand.css">All components automatically use your colors—no build step required.
Design systems should mark which tokens are safe to override:
{
"color": {
"primary": {
"$value": "#3b82f6",
"$customizable": true,
"$description": "Override with your brand color"
},
"primary-hover": {
"$value": "darken({color.primary}, 10%)",
"$description": "Computed - do not override directly"
}
}
}Generate a manifest of just the customization points:
tokenctl build --format=manifest:color --customizable-onlyOutput (for LLMs):
{
"tokens": {
"color.primary": {
"value": "#3b82f6",
"description": "Override with your brand color",
"customizable": true
},
"color.secondary": {
"value": "#8b5cf6",
"description": "Secondary brand color",
"customizable": true
}
}
}Non-customizable tokens (computed values, internal tokens) are excluded.
Prompt pattern for LLM-assisted customization:
You are customizing a design system.
You can ONLY modify tokens marked "customizable": true.
Available customization points:
{manifest.json contents}
The user wants: "Make it feel more playful with rounded corners"
Generate CSS overrides for @layer user.
LLM output:
@layer user {
:root {
--color-primary: oklch(70% 0.25 330);
--radius-btn: 9999px;
--radius-card: 1.5rem;
}
}CSS layer overrides work for 80% of cases. You need token-level merge only when:
- Computed values must recalculate - If you override
primaryand needprimary-hoverto recompute viadarken() - Manifest accuracy matters - LLMs need final resolved values including your overrides
- Validation of extensions - Check your overrides against layer rules
For these cases, use multi-directory merge: tokenctl build ./base ./overrides. See MERGE.md for details.
- Define all values as tokens
- Use semantic names (
primary, notblue-500in components) - Add descriptions and usage hints
- Validate with
--strict-layers - Generate category manifests for LLM efficiency
- Use fluid tokens (
clamp()) for smooth responsive scaling
- Use raw values in component definitions
- Skip the semantic layer
- Create tokens without descriptions
- Let components reference brand tokens directly
- Generate full catalogs when a category manifest suffices
| Layer | Naming Convention | Example |
|---|---|---|
| Brand | Descriptive of the value | blue-500, gray-100 |
| Semantic | Descriptive of purpose | primary, error, surface |
| Component | Descriptive of usage | btn-bg, card-shadow |
| Format | Use Case | Command |
|---|---|---|
tailwind |
Tailwind 4 projects | --format=tailwind |
css |
Non-Tailwind projects | --format=css |
catalog |
Full export for tools | --format=catalog |
manifest:CATEGORY |
LLM context efficiency | --format=manifest:color |
tokenctl transforms design system management from chaotic CSS to structured tokens:
- Define tokens in JSON with rich metadata
- Organize by layer (brand → semantic → component)
- Add responsive support with fluid values and breakpoint overrides
- Validate architecture with
--strict-layers - Generate output for Tailwind, pure CSS, or JSON manifests
- Enable LLMs with searchable, context-efficient token access
The result: consistent styling that humans and LLMs can both understand and use correctly.