Skip to main content

Design Tokens

Design tokens are the foundation of Stride’s theming system. They define colors, typography, spacing, and other design attributes as reusable values that cascade through the entire design system.

Token Architecture

Stride uses a two-tier token system:
  1. Core Tokens - Raw color values organized by palette (primary, neutral, success, etc.)
  2. Semantic Tokens - Contextual mappings that reference core tokens for specific use cases
This architecture allows brands to be customized at any level while maintaining consistency.

CSS Variable Naming Convention

All tokens are exposed as CSS custom properties (variables) with a consistent naming pattern:
/* Core tokens: --brand-{palette}-{shade} */
--brand-primary-500
--brand-neutral-900
--brand-success-600

/* Semantic tokens: --{category}-{variant} */
--text-primary
--bg-secondary
--interactive-primary-hover

/* Typography tokens: --font-{property} */
--font-family-primary
--font-weight-semibold

/* Layout tokens: --{property} */
--radius-button
--shadow-md
--spacing-scale

Core Token Categories

Core tokens define the raw color palette. From src/app/brands.css, here are examples from the Stride brand:

Primary Colors

Brand identity colors used for primary actions, links, and focus states.
.brand-stride {
  --brand-primary-50: #f0f9ff;   /* Lightest tint */
  --brand-primary-100: #e0f2fe;
  --brand-primary-200: #bae6fd;
  --brand-primary-300: #7dd3fc;
  --brand-primary-400: #38bdf8;
  --brand-primary-500: #0ea5e9;  /* Base color */
  --brand-primary-600: #0284c7;  /* Hover state */
  --brand-primary-700: #0369a1;  /* Active state */
  --brand-primary-800: #075985;
  --brand-primary-900: #0c4a6e;
  --brand-primary-950: #082f49;  /* Darkest shade */
}
Usage in dynamic brands:
import { registerDynamicBrand } from 'stride-ds';

registerDynamicBrand({
  id: 'custom-blue',
  name: 'Custom Blue',
  tokens: {
    core: {
      primary: {
        500: '#0ea5e9',  // Required: base color
        600: '#0284c7',  // Recommended: hover state
        700: '#0369a1',  // Recommended: active state
        50: '#f0f9ff',   // Optional: light tint for backgrounds
        100: '#e0f2fe'   // Optional: lighter tint
      }
    }
  }
});

Neutral Colors

Grays and neutrals used for text, borders, and backgrounds.
.brand-stride {
  --brand-neutral-0: #ffffff;    /* Pure white */
  --brand-neutral-50: #f8fafc;   /* Lightest gray */
  --brand-neutral-100: #f1f5f9;
  --brand-neutral-200: #e2e8f0;  /* Borders */
  --brand-neutral-300: #cbd5e1;
  --brand-neutral-400: #94a3b8;  /* Disabled states */
  --brand-neutral-500: #64748b;  /* Tertiary text */
  --brand-neutral-600: #475569;  /* Secondary text */
  --brand-neutral-700: #334155;
  --brand-neutral-800: #1e293b;
  --brand-neutral-900: #0f172a;  /* Primary text */
  --brand-neutral-950: #020617;  /* Darkest */
}
Shade usage guidelines:
  • 0, 50, 100: Backgrounds, subtle surfaces
  • 200, 300: Borders, dividers
  • 400, 500: Disabled states, tertiary text
  • 600, 700: Secondary text, icons
  • 800, 900, 950: Primary text, dark surfaces

Status Colors

Colors for success, warning, and error states.
/* Success - Green */
.brand-stride {
  --brand-success-50: #f0fdf4;   /* Light background */
  --brand-success-100: #dcfce7;  /* Subtle background */
  --brand-success-500: #22c55e;  /* Base color */
  --brand-success-600: #16a34a;  /* Hover state */
  --brand-success-700: #15803d;  /* Text on light bg */
}

/* Warning - Yellow/Orange */
.brand-stride {
  --brand-warning-50: #fffbeb;
  --brand-warning-100: #fef3c7;
  --brand-warning-500: #f59e0b;
  --brand-warning-600: #d97706;
  --brand-warning-700: #b45309;
}

/* Danger - Red */
.brand-stride {
  --brand-danger-50: #fef2f2;
  --brand-danger-100: #fee2e2;
  --brand-danger-500: #ef4444;
  --brand-danger-600: #dc2626;
  --brand-danger-700: #b91c1c;
}
Pattern for status colors:
  • 50: Background for success/warning/error alerts
  • 100: Subtle background variant
  • 500: Base color for icons, borders
  • 600: Hover state
  • 700: Text color on light backgrounds

Semantic Token Categories

Semantic tokens map core tokens to specific use cases. They automatically adapt to light/dark mode and brand changes.

Text Tokens

From src/app/brands.css:59:
.brand-stride {
  --text-primary: var(--brand-neutral-900);    /* Main body text */
  --text-secondary: var(--brand-neutral-600);  /* Supporting text */
  --text-tertiary: var(--brand-neutral-500);   /* Muted text */
  --text-inverse: var(--brand-neutral-0);      /* Text on dark bg */
  --text-disabled: var(--brand-neutral-400);   /* Disabled state */
  --text-link: var(--brand-primary-600);       /* Link color */
  --text-link-hover: var(--brand-primary-700); /* Link hover */
}
Usage in components:
// Direct in className
<p className="text-[var(--text-primary)]">
  Primary text color
</p>

// In custom CSS
.my-component {
  color: var(--text-primary);
}

// In dynamic brand
registerDynamicBrand({
  id: 'custom',
  name: 'Custom',
  tokens: {
    semantic: {
      textPrimary: '#1a1a1a',
      textLink: 'var(--brand-primary-600)',
      textLinkHover: 'color-mix(in srgb, var(--brand-primary-600) 80%, black)'
    }
  }
});

Background Tokens

From src/app/brands.css:67:
.brand-stride {
  --bg-primary: var(--brand-neutral-0);        /* Main background */
  --bg-secondary: var(--brand-neutral-50);     /* Secondary surface */
  --bg-tertiary: var(--brand-neutral-100);     /* Tertiary surface */
  --bg-inverse: var(--brand-neutral-900);      /* Dark background */
  --bg-disabled: var(--brand-neutral-100);     /* Disabled state */
  --bg-overlay: rgba(0, 0, 0, 0.6);           /* Modal overlay */
  --bg-brand: var(--brand-primary-500);        /* Brand-colored bg */
}
Background hierarchy:
<div className="bg-[var(--bg-primary)]">
  <div className="bg-[var(--bg-secondary)]">
    <div className="bg-[var(--bg-tertiary)]">
      Layered surfaces
    </div>
  </div>
</div>

Interactive Tokens

Tokens for buttons and interactive elements, from src/app/brands.css:81:
.brand-stride {
  /* Primary interactive (main buttons) */
  --interactive-primary: var(--brand-primary-600);
  --interactive-primary-hover: var(--brand-primary-700);
  --interactive-primary-active: var(--brand-primary-800);
  --interactive-primary-disabled: var(--brand-neutral-200);
  --interactive-primary-text: var(--brand-neutral-0);
  
  /* Secondary interactive (secondary buttons) */
  --interactive-secondary: var(--brand-neutral-100);
  --interactive-secondary-hover: var(--brand-neutral-200);
  --interactive-secondary-active: var(--brand-neutral-300);
  --interactive-secondary-disabled: var(--brand-neutral-100);
  
  /* Ghost interactive (ghost/text buttons) */
  --interactive-ghost: transparent;
  --interactive-ghost-hover: var(--brand-neutral-100);
  --interactive-ghost-active: var(--brand-neutral-200);
  --interactive-ghost-disabled: transparent;
}
Pattern for interactive states:
  • Base color
  • Hover (slightly darker)
  • Active (darkest - pressed state)
  • Disabled (muted)

Border Tokens

From src/app/brands.css:75:
.brand-stride {
  --border-primary: var(--brand-neutral-200);   /* Default borders */
  --border-secondary: var(--brand-neutral-300); /* Stronger borders */
  --border-strong: var(--brand-neutral-400);    /* Emphasized borders */
  --border-inverse: var(--brand-neutral-700);   /* Dark theme borders */
  --border-focus: var(--brand-primary-500);     /* Focus ring */
}

Status Tokens

Semantic tokens for status indicators, from src/app/brands.css:97:
.brand-stride {
  /* Success */
  --status-success: var(--brand-success-500);        /* Icon/border color */
  --status-success-hover: var(--brand-success-600);  /* Hover state */
  --status-success-bg: var(--brand-success-50);      /* Background */
  --status-success-text: var(--brand-success-700);   /* Text color */
  
  /* Warning */
  --status-warning: var(--brand-warning-500);
  --status-warning-hover: var(--brand-warning-600);
  --status-warning-bg: var(--brand-warning-50);
  --status-warning-text: var(--brand-warning-700);
  
  /* Danger */
  --status-danger: var(--brand-danger-500);
  --status-danger-hover: var(--brand-danger-600);
  --status-danger-bg: var(--brand-danger-50);
  --status-danger-text: var(--brand-danger-700);
}
Usage in Alert component:
<div 
  className="
    bg-[var(--status-success-bg)] 
    text-[var(--status-success-text)]
    border border-[var(--status-success)]
  "
>
  Success message
</div>

Typography Tokens

Font families and weights, from src/app/brands.css:54:
.brand-stride {
  --font-family-primary: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-family-secondary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
TypeScript interface (from src/lib/brands.ts:126):
interface TypographyBrandTokens {
  fontFamilyPrimary?: string;
  fontFamilySecondary?: string;
  fontWeightNormal?: string;      // Typically 400
  fontWeightMedium?: string;      // Typically 500
  fontWeightSemibold?: string;    // Typically 600
  fontWeightBold?: string;        // Typically 700
}
Example: Custom fonts
registerDynamicBrand({
  id: 'montserrat-brand',
  name: 'Montserrat Brand',
  tokens: {
    typography: {
      fontFamilyPrimary: '"Montserrat", sans-serif',
      fontFamilySecondary: '"Source Sans Pro", sans-serif',
      fontWeightMedium: '500',
      fontWeightSemibold: '600',
      fontWeightBold: '700'
    }
  }
});

Layout Tokens

Spacing, border radius, shadows, and transitions. TypeScript interface (from src/lib/brands.ts:136):
interface LayoutBrandTokens {
  spacingScale?: number;          // Multiplier for spacing
  radiusScale?: number;           // Multiplier for border radius
  radiusCard?: string;            // Card border radius
  radiusButton?: string;          // Button border radius
  radiusInput?: string;           // Input border radius
  shadowSm?: string;              // Small shadow
  shadowMd?: string;              // Medium shadow
  shadowLg?: string;              // Large shadow
  transitionFast?: string;        // Fast transition
  transitionNormal?: string;      // Normal transition
  transitionSlow?: string;        // Slow transition
}
Example: Forest brand spacing (from src/app/brands.css:334):
.brand-forest {
  --spacing-scale: 1.2;
  --spacing-xs: calc(0.25rem * var(--spacing-scale));   /* 0.3rem */
  --spacing-sm: calc(0.5rem * var(--spacing-scale));    /* 0.6rem */
  --spacing-md: calc(0.75rem * var(--spacing-scale));   /* 0.9rem */
  --spacing-lg: calc(1rem * var(--spacing-scale));      /* 1.2rem */
  --spacing-xl: calc(1.5rem * var(--spacing-scale));    /* 1.8rem */
  --spacing-2xl: calc(2rem * var(--spacing-scale));     /* 2.4rem */
}
Example: Coral brand - no button radius (from src/app/brands.css:222):
.brand-coral {
  --radius-button: 0;  /* Square buttons */
}
Example: Dynamic layout tokens
registerDynamicBrand({
  id: 'sharp-design',
  name: 'Sharp Design',
  tokens: {
    layout: {
      radiusButton: '0',              // Square buttons
      radiusCard: '4px',              // Subtle card radius
      radiusInput: '0',               // Square inputs
      shadowMd: '0 2px 8px rgba(0, 0, 0, 0.1)',
      transitionFast: '100ms ease'
    }
  }
});

Defining Custom Tokens

Add brand-specific CSS variables that aren’t part of the standard token system:
registerDynamicBrand({
  id: 'extended-tokens',
  name: 'Extended Tokens',
  tokens: {
    custom: {
      // Custom variables (auto-prefixed with --)
      'header-height': '80px',
      'sidebar-width': '280px',
      'nav-background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
      'card-hover-lift': '4px',
      'brand-accent': '#ff6b35',
      'focus-ring-width': '3px',
      'focus-ring-offset': '2px'
    }
  }
});
Using custom tokens:
.header {
  height: var(--header-height);
  background: var(--nav-background);
}

.sidebar {
  width: var(--sidebar-width);
}

.card:hover {
  transform: translateY(calc(-1 * var(--card-hover-lift)));
}

Semantic Mapping Patterns

Best practices for mapping semantic tokens to core tokens.

Pattern 1: Direct Reference

Most common - reference core tokens directly:
semantic: {
  textPrimary: 'var(--brand-neutral-900)',
  textLink: 'var(--brand-primary-600)',
  bgPrimary: 'var(--brand-neutral-0)',
  borderPrimary: 'var(--brand-neutral-200)'
}

Pattern 2: Color Mixing

Create variations using CSS color-mix:
semantic: {
  textLink: 'var(--brand-primary-600)',
  // 20% darker on hover
  textLinkHover: 'color-mix(in srgb, var(--brand-primary-600) 80%, black)',
  // Semi-transparent overlay
  bgOverlay: 'color-mix(in srgb, var(--brand-neutral-900) 60%, transparent)'
}

Pattern 3: Token Chaining

Reference other semantic tokens:
semantic: {
  textLink: 'var(--brand-primary-600)',
  textLinkHover: 'var(--brand-primary-700)',
  // Chain to textLink
  borderFocus: 'var(--text-link)',
  interactivePrimary: 'var(--text-link)'
}

Pattern 4: Hardcoded Values

Use when a specific value is needed regardless of core palette:
semantic: {
  textPrimary: '#000000',           // Pure black
  bgPrimary: '#ffffff',             // Pure white
  bgOverlay: 'rgba(0, 0, 0, 0.75)' // Fixed opacity overlay
}

Dark Mode Support

Semantic tokens automatically adapt to dark mode. From src/app/brands.css:588:
/* Light mode (default) */
.brand-stride {
  --text-primary: var(--brand-neutral-900);  /* Dark text */
  --bg-primary: var(--brand-neutral-0);      /* Light background */
}

/* Dark mode */
.brand-stride.dark {
  --text-primary: var(--brand-neutral-50);   /* Light text */
  --bg-primary: var(--brand-neutral-900);    /* Dark background */
}
This remapping happens automatically - components using --text-primary will adapt to both light and dark modes without any changes.

Token Resolution Order

When applying a dynamic brand, tokens are resolved in this order:
  1. Custom tokens - Applied first (highest priority)
  2. Layout tokens - Applied next
  3. Typography tokens - Then typography
  4. Semantic tokens - Then semantic mappings
  5. Core tokens - Base color definitions
  6. Fallback brand - Used for any undefined tokens
From src/lib/brands.ts:394, the token application logic:
const updateDynamicBrandStyles = (brand: DynamicBrandConfig): void => {
  const cssRules: string[] = [];
  
  // 1. Core tokens
  if (brand.tokens.core) {
    // Apply primary, neutral, success, warning, danger
  }
  
  // 2. Semantic tokens
  if (brand.tokens.semantic) {
    // Apply text, bg, interactive, border, status tokens
  }
  
  // 3. Typography tokens
  if (brand.tokens.typography) {
    // Apply font families and weights
  }
  
  // 4. Layout tokens
  if (brand.tokens.layout) {
    // Apply spacing, radius, shadow, transition
  }
  
  // 5. Custom tokens
  if (brand.tokens.custom) {
    // Apply any custom CSS variables
  }
};

TypeScript Types

Complete type definitions from src/lib/brands.ts:
// Core color tokens (lines 14-67)
export interface CoreBrandTokens {
  primary?: { 50?: string; 100?: string; /* ... */ 950?: string; };
  neutral?: { 0?: string; 50?: string; /* ... */ 950?: string; };
  success?: { 50?: string; 100?: string; 500?: string; 600?: string; 700?: string; };
  warning?: { 50?: string; 100?: string; 500?: string; 600?: string; 700?: string; };
  danger?: { 50?: string; 100?: string; 500?: string; 600?: string; 700?: string; };
}

// Semantic tokens (lines 69-124)
export interface SemanticBrandTokens {
  textPrimary?: string; textSecondary?: string; /* ... */
  bgPrimary?: string; bgSecondary?: string; /* ... */
  interactivePrimary?: string; interactivePrimaryHover?: string; /* ... */
  borderPrimary?: string; borderSecondary?: string; /* ... */
  statusSuccess?: string; statusSuccessBg?: string; /* ... */
  surfaceCard?: string; surfaceModal?: string; /* ... */
}

// Typography tokens (lines 126-134)
export interface TypographyBrandTokens {
  fontFamilyPrimary?: string;
  fontFamilySecondary?: string;
  fontWeightNormal?: string;
  fontWeightMedium?: string;
  fontWeightSemibold?: string;
  fontWeightBold?: string;
}

// Layout tokens (lines 136-149)
export interface LayoutBrandTokens {
  spacingScale?: number;
  radiusScale?: number;
  radiusCard?: string;
  radiusButton?: string;
  radiusInput?: string;
  shadowSm?: string;
  shadowMd?: string;
  shadowLg?: string;
  transitionFast?: string;
  transitionNormal?: string;
  transitionSlow?: string;
}

Best Practices

  1. Always use semantic tokens in components
    // Good
    <p className="text-[var(--text-primary)]">
    
    // Avoid
    <p className="text-[var(--brand-neutral-900)]">
    
  2. Provide complete interactive state tokens
    semantic: {
      interactivePrimary: 'var(--brand-primary-600)',
      interactivePrimaryHover: 'var(--brand-primary-700)',    // Required
      interactivePrimaryActive: 'var(--brand-primary-800)',   // Required
      interactivePrimaryDisabled: 'var(--brand-neutral-300)'  // Required
    }
    
  3. Use color-mix for variations
    // Instead of hardcoding hover color
    interactivePrimaryHover: 'color-mix(in srgb, var(--brand-primary-600) 80%, black)'
    
  4. Leverage fallback for missing tokens
    // Only define what you need to customize
    tokens: {
      core: {
        primary: { 500: '#custom' }  // Other shades from fallback
      }
    }
    
  5. Use descriptive custom token names
    custom: {
      'dashboard-sidebar-width': '280px',  // Good
      'val1': '280px'                      // Avoid
    }
    

Next Steps

Build docs developers (and LLMs) love