Skip to main content

Creating Custom Brands

The Stride Design System includes a powerful dynamic brand system that allows you to create custom white-label brands at runtime without modifying the core design system code.

Overview

Dynamic brands enable you to:
  • Create unlimited custom brands programmatically
  • Override design tokens selectively (all tokens are optional)
  • Use intelligent fallback for undefined tokens
  • Persist brands automatically in localStorage
  • Switch between brands with smooth transitions

Registering a Dynamic Brand

Use the registerDynamicBrand() function to create a new brand. This function is available from both the main library and the useBrand hook.

Basic Registration

import { registerDynamicBrand } from 'stride-ds';

registerDynamicBrand({
  id: 'client-abc',
  name: 'ABC Corporation',
  description: 'Custom brand for ABC Corp',
  tokens: {
    core: {
      primary: {
        500: '#ff6b35',
        600: '#e85d2a',
        700: '#d14f1f'
      }
    }
  }
});
Function Signature (from src/lib/brands.ts:254)
export const registerDynamicBrand = (config: DynamicBrandConfig): void

DynamicBrandConfig Interface

The complete configuration interface for dynamic brands:
interface DynamicBrandConfig {
  id: string;                    // Required: Unique brand identifier (alphanumeric, hyphens, underscores)
  name: string;                  // Required: Display name
  description?: string;          // Optional: Brand description
  
  tokens: {                      // Required: Token definitions
    core?: CoreBrandTokens;      // Core color tokens (all optional)
    semantic?: SemanticBrandTokens;  // Semantic tokens (all optional)
    typography?: TypographyBrandTokens;  // Typography tokens (all optional)
    layout?: LayoutBrandTokens;  // Layout tokens (all optional)
    custom?: Record<string, string>;  // Custom CSS variables
  };
  
  fallback?: {                   // Optional fallback configuration
    brand?: string;              // Fallback brand ID (default: 'stride')
    useSemanticFallback?: boolean;  // Use semantic tokens from fallback
  };
}

Token Structure

Core Tokens

Core tokens define the base color palette. All tokens are optional - define only what you need to customize.
interface CoreBrandTokens {
  primary?: {     // Primary brand colors
    50?: string;  // Lightest
    100?: string;
    200?: string;
    300?: string;
    400?: string;
    500?: string; // Base color
    600?: string;
    700?: string;
    800?: string;
    900?: string;
    950?: string; // Darkest
  };
  
  neutral?: {     // Neutral/gray colors
    0?: string;   // Pure white
    50?: string;
    100?: string;
    200?: string;
    300?: string;
    400?: string;
    500?: string;
    600?: string;
    700?: string;
    800?: string;
    900?: string;
    950?: string;
  };
  
  success?: {     // Success state colors
    50?: string;
    100?: string;
    500?: string;
    600?: string;
    700?: string;
  };
  
  warning?: {     // Warning state colors
    50?: string;
    100?: string;
    500?: string;
    600?: string;
    700?: string;
  };
  
  danger?: {      // Error/danger state colors
    50?: string;
    100?: string;
    500?: string;
    600?: string;
    700?: string;
  };
}
Example: Defining core tokens
registerDynamicBrand({
  id: 'coral-tech',
  name: 'Coral Tech',
  tokens: {
    core: {
      primary: {
        500: '#f97316',  // Orange base
        600: '#ea580c',
        700: '#c2410c'
      },
      neutral: {
        0: '#ffffff',
        900: '#1a1a1a'
      },
      // success, warning, danger omitted - will use fallback brand
    }
  }
});

Semantic Tokens

Semantic tokens map to specific use cases (text, backgrounds, borders, etc.). They can reference core tokens using CSS variables, use hardcoded values, or use CSS color functions.
interface SemanticBrandTokens {
  // Text colors
  textPrimary?: string;       // Main text color
  textSecondary?: string;     // Secondary text
  textTertiary?: string;      // Tertiary/muted text
  textInverse?: string;       // Text on dark backgrounds
  textDisabled?: string;      // Disabled state text
  textLink?: string;          // Link color
  textLinkHover?: string;     // Link hover state
  textBrand?: string;         // Brand-colored text
  
  // Background colors
  bgPrimary?: string;         // Main background
  bgSecondary?: string;       // Secondary background
  bgTertiary?: string;        // Tertiary background
  bgInverse?: string;         // Inverted background
  bgDisabled?: string;        // Disabled state background
  bgOverlay?: string;         // Modal/overlay background
  bgTooltip?: string;         // Tooltip background
  
  // Interactive colors
  interactivePrimary?: string;
  interactivePrimaryHover?: string;
  interactivePrimaryActive?: string;
  interactivePrimaryDisabled?: string;
  interactiveSecondary?: string;
  interactiveSecondaryHover?: string;
  interactiveSecondaryActive?: string;
  interactiveDisabled?: string;
  
  // Border colors
  borderPrimary?: string;
  borderSecondary?: string;
  borderFocus?: string;
  borderError?: string;
  borderSuccess?: string;
  borderWarning?: string;
  
  // Status colors
  statusSuccess?: string;
  statusSuccessBg?: string;
  statusSuccessText?: string;
  statusWarning?: string;
  statusWarningBg?: string;
  statusWarningText?: string;
  statusDanger?: string;
  statusDangerBg?: string;
  statusDangerText?: string;
  
  // Surface colors
  surfaceCard?: string;
  surfaceModal?: string;
  surfacePopover?: string;
  surfaceTooltip?: string;
}
Example: Three ways to define semantic tokens
registerDynamicBrand({
  id: 'flexible-brand',
  name: 'Flexible Brand',
  tokens: {
    core: {
      primary: { 500: '#3b82f6', 600: '#2563eb' }
    },
    semantic: {
      // 1. Direct hardcoded value
      textPrimary: '#1a1a1a',
      
      // 2. Reference to core token (preferred)
      textLink: 'var(--brand-primary-600)',
      interactivePrimary: 'var(--brand-primary-500)',
      
      // 3. CSS color function
      textLinkHover: 'color-mix(in srgb, var(--brand-primary-600) 80%, black)',
      
      // 4. Reference to another semantic token
      borderFocus: 'var(--text-link)'
    }
  }
});

Typography Tokens

Customize fonts and font weights:
interface TypographyBrandTokens {
  fontFamilyPrimary?: string;     // Main font family
  fontFamilySecondary?: string;   // Secondary font family
  fontWeightNormal?: string;      // Normal weight (typically 400)
  fontWeightMedium?: string;      // Medium weight (typically 500)
  fontWeightSemibold?: string;    // Semibold weight (typically 600)
  fontWeightBold?: string;        // Bold weight (typically 700)
}
Example: Custom typography
registerDynamicBrand({
  id: 'montserrat-brand',
  name: 'Montserrat Brand',
  tokens: {
    typography: {
      fontFamilyPrimary: '"Montserrat", sans-serif',
      fontFamilySecondary: '"Roboto", sans-serif',
      fontWeightSemibold: '600',
      fontWeightBold: '700'
    }
  }
});

Layout Tokens

Customize spacing, border radius, shadows, and transitions:
interface LayoutBrandTokens {
  spacingScale?: number;          // Scale multiplier for spacing
  radiusScale?: number;           // Scale 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 duration
  transitionNormal?: string;      // Normal transition duration
  transitionSlow?: string;        // Slow transition duration
}
Example: Generous spacing and no button radius
registerDynamicBrand({
  id: 'spacious-brand',
  name: 'Spacious Brand',
  tokens: {
    layout: {
      spacingScale: 1.2,           // 20% more spacing
      radiusButton: '0',           // Square buttons
      radiusCard: '12px',          // Rounded cards
      shadowMd: '0 4px 6px rgba(0, 0, 0, 0.1)'
    }
  }
});

Custom Tokens

Define any custom CSS variables for brand-specific needs:
registerDynamicBrand({
  id: 'custom-vars',
  name: 'Custom Variables',
  tokens: {
    custom: {
      'header-height': '80px',
      'sidebar-width': '280px',
      'brand-accent': '#ff6b35',
      'card-hover-lift': '4px'
    }
  }
});

// Use in CSS or components
// var(--header-height)
// var(--sidebar-width)

Complete Example

Here’s a comprehensive example from the source code (src/stories/DynamicBrandSystem.stories.tsx:58):
import { registerDynamicBrand, useBrand } from 'stride-ds';

registerDynamicBrand({
  id: 'client-abc',
  name: 'ABC Corporation',
  description: 'Custom white-label brand for ABC Corp',
  tokens: {
    core: {
      primary: {
        100: '#ffe6f0',
        500: '#ff6b35',
        600: '#e85d2a',
        700: '#d14f1f'
      },
      neutral: {
        0: '#ffffff',
        100: '#f5f5f5',
        900: '#1a1a1a'
      }
    },
    semantic: {
      textPrimary: '#1a1a1a',
      textLink: 'var(--brand-primary-600)',
      textLinkHover: 'color-mix(in srgb, var(--brand-primary-600) 80%, black)',
      bgPrimary: 'var(--brand-neutral-0)',
      interactivePrimary: 'var(--brand-primary-600)',
      interactivePrimaryHover: 'var(--brand-primary-700)',
      borderFocus: 'var(--brand-primary-500)'
    },
    typography: {
      fontFamilyPrimary: '"Montserrat", sans-serif',
      fontWeightSemibold: '600'
    },
    layout: {
      radiusButton: '8px',
      shadowMd: '0 4px 12px rgba(0, 0, 0, 0.1)'
    },
    custom: {
      'header-height': '80px',
      'logo-max-width': '200px'
    }
  },
  fallback: {
    brand: 'stride',              // Use Stride for undefined tokens
    useSemanticFallback: true     // Inherit semantic mappings
  }
});

// Apply the brand
const { setBrand } = useBrand();
setBrand('client-abc');

Validating Brand Configuration

Use validateDynamicBrandConfig() to check if your brand configuration is valid before registering: Function Signature (from src/lib/brands.ts:683)
export const validateDynamicBrandConfig = (
  config: DynamicBrandConfig
): { valid: boolean; errors: string[] }
Example usage:
import { validateDynamicBrandConfig, registerDynamicBrand } from 'stride-ds';

const brandConfig: DynamicBrandConfig = {
  id: 'test-brand',
  name: 'Test Brand',
  tokens: {
    core: {
      primary: { 500: '#invalid-color' }
    }
  }
};

const validation = validateDynamicBrandConfig(brandConfig);

if (validation.valid) {
  registerDynamicBrand(brandConfig);
  console.log('Brand registered successfully');
} else {
  console.error('Validation errors:', validation.errors);
  // Output: ["Invalid primary color for shade 500: #invalid-color"]
}

Using with React Hook

The useBrand hook provides a convenient way to manage brands in React components:
import { useBrand } from 'stride-ds';

function BrandManager() {
  const { registerDynamicBrand, setBrand } = useBrand();
  
  const createClientBrand = () => {
    registerDynamicBrand({
      id: 'new-client',
      name: 'New Client',
      tokens: {
        core: {
          primary: { 500: '#9333ea' }
        }
      }
    });
    
    // Immediately apply the new brand
    setBrand('new-client');
  };
  
  return (
    <button onClick={createClientBrand}>
      Create & Apply Brand
    </button>
  );
}

Fallback System

The fallback system ensures your brand always has complete token coverage:

How Fallback Works

  1. Core tokens: If a specific shade is not defined, the system looks for it in the fallback brand
  2. Semantic tokens: If useSemanticFallback: true, undefined semantic tokens use the fallback brand’s semantic mapping
  3. Default fallback: If no fallback is specified, the system uses the configured defaultFallbackBrand (typically ‘stride’)

Configuring Fallback

// Option 1: Use default fallback (stride)
registerDynamicBrand({
  id: 'minimal-brand',
  name: 'Minimal Brand',
  tokens: {
    core: {
      primary: { 500: '#3b82f6' }
    }
  }
  // No fallback specified - uses 'stride' by default
});

// Option 2: Specify custom fallback brand
registerDynamicBrand({
  id: 'forest-based',
  name: 'Forest Based Brand',
  tokens: {
    core: {
      primary: { 500: '#10b981' }
    }
  },
  fallback: {
    brand: 'forest',              // Use forest brand for missing tokens
    useSemanticFallback: true
  }
});

// Option 3: Disable semantic fallback
registerDynamicBrand({
  id: 'independent-brand',
  name: 'Independent Brand',
  tokens: {
    core: { /* ... */ },
    semantic: { /* ... */ }
  },
  fallback: {
    brand: 'stride',
    useSemanticFallback: false    // Only use core tokens from fallback
  }
});

Best Practices

  1. Use descriptive IDs: Use kebab-case IDs that clearly identify the brand
    // Good
    id: 'acme-corp-2024'
    id: 'client-abc'
    
    // Avoid
    id: 'brand1'
    id: 'test'
    
  2. Prefer CSS variable references: Reference core tokens in semantic tokens for consistency
    // Good - references core token
    textLink: 'var(--brand-primary-600)'
    
    // Less flexible - hardcoded
    textLink: '#2563eb'
    
  3. Define complete color scales: When defining core tokens, provide at least 500, 600, 700 for interactive states
    primary: {
      500: '#3b82f6',  // Base
      600: '#2563eb',  // Hover
      700: '#1d4ed8'   // Active
    }
    
  4. Validate before production: Always validate brand configs before deploying
    const validation = validateDynamicBrandConfig(config);
    if (!validation.valid) {
      throw new Error(`Invalid brand config: ${validation.errors.join(', ')}`);
    }
    
  5. Use semantic tokens for components: Don’t reference core tokens directly in components
    // Good - uses semantic token
    color: var(--text-primary)
    
    // Avoid - uses core token directly
    color: var(--brand-neutral-900)
    

Next Steps

Build docs developers (and LLMs) love