Skip to main content
Dynamic brands enable you to create custom white-label brands at runtime without writing CSS. The system automatically generates CSS variables and provides intelligent fallbacks for undefined tokens.

Quick Start

import { registerDynamicBrand, applyBrandTheme } from 'stride-ds';

registerDynamicBrand({
  id: 'my-brand',
  name: 'My Custom Brand',
  tokens: {
    core: {
      primary: {
        500: '#7c3aed',
        600: '#6d28d9',
        700: '#5b21b6',
      }
    }
  },
  fallback: {
    brand: 'stride',
    useSemanticFallback: true,
  }
});

applyBrandTheme('my-brand');
This creates a functional brand with just 3 color definitions. All other tokens fall back to Stride.

Configuration Interface

DynamicBrandConfig

The complete configuration object:
interface DynamicBrandConfig {
  id: string;                    // Unique brand ID (alphanumeric, -, _)
  name: string;                  // Display name
  description?: string;          // Optional description
  
  tokens: {
    core?: CoreBrandTokens;      // Color palettes
    semantic?: SemanticBrandTokens;  // Semantic mappings
    typography?: TypographyBrandTokens; // Fonts
    layout?: LayoutBrandTokens;  // Spacing, shadows, etc.
    custom?: Record<string, string>; // Custom CSS variables
  };
  
  fallback?: {
    brand?: string;              // Fallback brand (default: 'stride')
    useSemanticFallback?: boolean; // Inherit semantic tokens (default: true)
  };
}

CoreBrandTokens

Base color palettes (all optional):
interface CoreBrandTokens {
  primary?: {
    50?: string;   // Lightest
    100?: string;
    200?: string;
    300?: string;
    400?: string;
    500?: string;  // Main brand color
    600?: string;
    700?: string;
    800?: string;
    900?: string;
    950?: string;  // Darkest
  };
  
  neutral?: {
    0?: string;    // Pure white
    50?: string;
    // ... through 950
  };
  
  success?: {
    50?: string;
    100?: string;
    500?: string;
    600?: string;
    700?: string;
  };
  
  warning?: { /* Same structure */ };
  danger?: { /* Same structure */ };
}

SemanticBrandTokens

Purpose-driven tokens (all optional):
interface SemanticBrandTokens {
  // Text (8 tokens)
  textPrimary?: string;           // Main text color
  textSecondary?: string;         // Secondary text
  textTertiary?: string;          // Tertiary text
  textInverse?: string;           // Text on dark backgrounds
  textDisabled?: string;          // Disabled state
  textLink?: string;              // Link color
  textLinkHover?: string;         // Link hover
  textBrand?: string;             // Brand-colored text
  
  // Backgrounds (7 tokens)
  bgPrimary?: string;             // Main background
  bgSecondary?: string;           // Secondary background
  bgTertiary?: string;            // Tertiary background
  bgInverse?: string;             // Inverse background
  bgDisabled?: string;            // Disabled background
  bgOverlay?: string;             // Modal overlays
  bgTooltip?: string;             // Tooltip background
  
  // Interactive (8 tokens)
  interactivePrimary?: string;    // Primary button bg
  interactivePrimaryHover?: string;
  interactivePrimaryActive?: string;
  interactivePrimaryDisabled?: string;
  interactiveSecondary?: string;  // Secondary button bg
  interactiveSecondaryHover?: string;
  interactiveSecondaryActive?: string;
  interactiveDisabled?: string;
  
  // Borders (6 tokens)
  borderPrimary?: string;
  borderSecondary?: string;
  borderFocus?: string;
  borderError?: string;
  borderSuccess?: string;
  borderWarning?: string;
  
  // Status (9 tokens)
  statusSuccess?: string;
  statusSuccessBg?: string;
  statusSuccessText?: string;
  statusWarning?: string;
  statusWarningBg?: string;
  statusWarningText?: string;
  statusDanger?: string;
  statusDangerBg?: string;
  statusDangerText?: string;
  
  // Surfaces (4 tokens)
  surfaceCard?: string;
  surfaceModal?: string;
  surfacePopover?: string;
  surfaceTooltip?: string;
}

TypographyBrandTokens

interface TypographyBrandTokens {
  fontFamilyPrimary?: string;     // 'Inter, sans-serif'
  fontFamilySecondary?: string;   // 'Roboto, sans-serif'
  fontWeightNormal?: string;      // '400'
  fontWeightMedium?: string;      // '500'
  fontWeightSemibold?: string;    // '600'
  fontWeightBold?: string;        // '700'
}

LayoutBrandTokens

interface LayoutBrandTokens {
  spacingScale?: number;          // Multiplier (e.g., 1.2)
  radiusScale?: number;           // Multiplier
  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;        // '150ms ease'
  transitionNormal?: string;      // '200ms ease'
  transitionSlow?: string;        // '300ms ease'
}

Registration & Management

Register a Dynamic Brand

import { registerDynamicBrand } from 'stride-ds';

registerDynamicBrand({
  id: 'custom-corp',
  name: 'Custom Corporation',
  description: 'Custom white-label brand',
  tokens: {
    core: {
      primary: {
        500: '#ec4899',
        600: '#db2777',
      }
    },
    typography: {
      fontFamilyPrimary: 'Montserrat, sans-serif',
    }
  }
});
This function:
  1. Validates the config
  2. Stores the brand in a registry
  3. Saves to localStorage (if enabled)
  4. Logs success message

Get a Dynamic Brand

import { getDynamicBrand, isDynamicBrand } from 'stride-ds';

const brand = getDynamicBrand('custom-corp');
// Returns DynamicBrandConfig | undefined

if (isDynamicBrand('custom-corp')) {
  console.log('This is a dynamic brand');
}

Unregister a Dynamic Brand

import { unregisterDynamicBrand } from 'stride-ds';

const success = unregisterDynamicBrand('custom-corp');
// Returns boolean

// Automatically:
// - Removes from registry
// - Clears localStorage
// - Removes generated CSS
// - Reverts to fallback if active

List All Dynamic Brands

import { getAllDynamicBrands } from 'stride-ds';

const brands = getAllDynamicBrands();
// Returns DynamicBrandConfig[]

brands.forEach(brand => {
  console.log(brand.id, brand.name);
});

Clear All Dynamic Brands

import { clearAllDynamicBrands } from 'stride-ds';

clearAllDynamicBrands();
// Unregisters all dynamic brands

Fallback System

The fallback system fills in undefined tokens from a static brand.

How Fallbacks Work

registerDynamicBrand({
  id: 'minimal-brand',
  name: 'Minimal Brand',
  tokens: {
    core: {
      primary: { 500: '#8b5cf6' } // Only define primary color
    }
  },
  fallback: {
    brand: 'stride',           // Use Stride for everything else
    useSemanticFallback: true, // Inherit semantic token mappings
  }
});
Generates:
/* Applied classes */
.brand-stride .brand-dynamic-minimal-brand { }

/* CSS cascade handles the rest */
.brand-stride {
  /* All Stride tokens are available as fallbacks */
  --brand-primary-500: #0ea5e9; /* Overridden by dynamic */
  --brand-neutral-900: #0f172a; /* Used as fallback */
  --text-primary: var(--brand-neutral-900); /* Used as fallback */
}

.brand-dynamic-minimal-brand {
  /* Only the defined token */
  --brand-primary-500: #8b5cf6;
}

Semantic Fallback Behavior

With useSemanticFallback: true:
tokens: {
  core: {
    primary: { 500: '#8b5cf6' }
  },
  // semantic not defined
}
Result:
  • --text-link inherits from fallback brand (which maps to --brand-primary-600)
  • --interactive-primary inherits from fallback brand
  • All 40+ semantic tokens are available automatically
With useSemanticFallback: false:
  • Only explicitly defined tokens are available
  • Undefined tokens have no value (may cause visual issues)

Custom Fallback Brand

registerDynamicBrand({
  id: 'green-themed',
  name: 'Green Theme',
  tokens: {
    core: {
      primary: { 500: '#10b981' }
    }
  },
  fallback: {
    brand: 'forest', // Use Forest instead of Stride
    useSemanticFallback: true,
  }
});

Default Fallback Configuration

Set the global default fallback:
import { configureDynamicBrandSystem } from 'stride-ds';

configureDynamicBrandSystem({
  defaultFallbackBrand: 'coral', // Default is 'stride'
});

// Now all dynamic brands without explicit fallback use Coral
registerDynamicBrand({
  id: 'my-brand',
  name: 'My Brand',
  tokens: { /* ... */ }
  // Implicitly uses 'coral' as fallback
});

Real-World Examples

Example 1: Minimal Brand (Primary Color Only)

registerDynamicBrand({
  id: 'simple-purple',
  name: 'Simple Purple',
  tokens: {
    core: {
      primary: {
        500: '#a855f7',
        600: '#9333ea',
        700: '#7e22ce',
      }
    }
  },
  fallback: {
    brand: 'stride',
    useSemanticFallback: true,
  }
});

applyBrandTheme('simple-purple');
Result: Full-featured brand with purple accent color and Stride foundation.

Example 2: Typography Override

registerDynamicBrand({
  id: 'custom-font',
  name: 'Custom Font Brand',
  tokens: {
    typography: {
      fontFamilyPrimary: 'Gilroy, sans-serif',
      fontFamilySecondary: 'SF Mono, monospace',
      fontWeightBold: '800', // Extra bold
    }
  },
  fallback: {
    brand: 'stride',
    useSemanticFallback: true,
  }
});
Result: Stride brand with custom fonts.

Example 3: Complete Custom Brand

registerDynamicBrand({
  id: 'full-custom',
  name: 'Full Custom Brand',
  tokens: {
    core: {
      primary: {
        50: '#fdf2f8',
        100: '#fce7f3',
        500: '#ec4899',
        600: '#db2777',
        700: '#be185d',
      },
      neutral: {
        0: '#ffffff',
        100: '#f5f5f5',
        900: '#171717',
      }
    },
    semantic: {
      textPrimary: '#171717',
      textLink: '#ec4899',
      interactivePrimary: '#db2777',
      interactivePrimaryHover: '#be185d',
      borderFocus: '#ec4899',
    },
    typography: {
      fontFamilyPrimary: 'Poppins, sans-serif',
      fontWeightBold: '700',
    },
    layout: {
      radiusButton: '0.5rem',
      shadowMd: '0 4px 12px rgba(236, 72, 153, 0.1)',
    },
    custom: {
      'brand-gradient': 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)',
      'brand-glow': '0 0 20px rgba(236, 72, 153, 0.3)',
    }
  },
  fallback: {
    brand: 'stride',
    useSemanticFallback: false, // We defined everything
  }
});
Using custom variables:
.my-component {
  background: var(--brand-gradient);
  box-shadow: var(--brand-glow);
}

Example 4: Multi-Tenant SaaS

function registerTenantBrand(tenantId: string, brandColor: string) {
  registerDynamicBrand({
    id: `tenant-${tenantId}`,
    name: `Tenant ${tenantId}`,
    tokens: {
      core: {
        primary: {
          500: brandColor,
          // Auto-generate shades with a color library
        }
      }
    },
    fallback: {
      brand: 'stride',
      useSemanticFallback: true,
    }
  });
}

// Register multiple tenants
registerTenantBrand('acme', '#7c3aed');
registerTenantBrand('globex', '#ef4444');
registerTenantBrand('initech', '#14b8a6');

// Switch based on current tenant
function loadTenantBrand(tenantId: string) {
  applyBrandTheme(`tenant-${tenantId}`);
}

System Configuration

import { configureDynamicBrandSystem } from 'stride-ds';

configureDynamicBrandSystem({
  defaultFallbackBrand: 'stride',  // Default fallback brand
  enableLocalStorage: true,        // Persist brands to localStorage
  enableTransitions: true,         // Smooth brand switching
  transitionDuration: 50,          // Transition duration in ms
});

Configuration Options

interface DynamicBrandSystemConfig {
  defaultFallbackBrand: string;    // Default: 'stride'
  enableLocalStorage: boolean;     // Default: true
  enableTransitions: boolean;      // Default: true
  transitionDuration: number;      // Default: 50 (ms)
}

Get Current Configuration

import { getDynamicBrandSystemConfig } from 'stride-ds';

const config = getDynamicBrandSystemConfig();
console.log(config.defaultFallbackBrand); // 'stride'

Persistence & Restoration

Dynamic brands are automatically saved to localStorage and restored on page load.

Automatic Restoration

import { initializeBrand } from 'stride-ds';

// Call once on app startup
initializeBrand();

// This function:
// 1. Restores all registered dynamic brands from localStorage
// 2. Applies the last active brand (static or dynamic)

Manual Restoration

import { restoreDynamicBrandsFromStorage } from 'stride-ds';

// Restore all dynamic brands from storage
restoreDynamicBrandsFromStorage();

Disable Persistence

configureDynamicBrandSystem({
  enableLocalStorage: false,
});

// Now brands are not saved/restored

Validation

import { validateDynamicBrandConfig } from 'stride-ds';

const config: DynamicBrandConfig = { /* ... */ };

const result = validateDynamicBrandConfig(config);

if (result.valid) {
  registerDynamicBrand(config);
} else {
  console.error('Validation errors:', result.errors);
  // [
  //   'Brand ID is required and must be a string',
  //   'Invalid primary color for shade 500: notacolor'
  // ]
}

React Hook Integration

import { useBrand } from 'stride-ds';

function BrandManager() {
  const {
    dynamicBrands,
    registerDynamicBrand,
    unregisterDynamicBrand,
    isDynamicBrand,
    setBrand,
  } = useBrand();

  const handleCreate = (config: DynamicBrandConfig) => {
    registerDynamicBrand(config);
    setBrand(config.id);
  };

  const handleDelete = (brandId: string) => {
    if (isDynamicBrand(brandId)) {
      unregisterDynamicBrand(brandId);
    }
  };

  return (
    <div>
      {dynamicBrands.map(brand => (
        <div key={brand.id}>
          {brand.name}
          <button onClick={() => setBrand(brand.id)}>Apply</button>
          <button onClick={() => handleDelete(brand.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}

CSS Output

Dynamic brands generate CSS automatically:
registerDynamicBrand({
  id: 'example',
  name: 'Example',
  tokens: {
    core: { primary: { 500: '#7c3aed' } },
    semantic: { textBrand: '#7c3aed' },
    typography: { fontFamilyPrimary: 'Inter, sans-serif' },
  }
});
Generates in <head>:
/* <style id="dynamic-brand-example"> */
.brand-dynamic-example {
  --brand-primary-500: #7c3aed;
  --text-brand: #7c3aed;
  --font-family-primary: Inter, sans-serif;
}
Applied to HTML:
<html class="brand-stride brand-dynamic-example">
  <!-- Stride provides fallbacks, example provides overrides -->
</html>

TypeScript Types

import type {
  DynamicBrandConfig,
  CoreBrandTokens,
  SemanticBrandTokens,
  TypographyBrandTokens,
  LayoutBrandTokens,
  DynamicBrandSystemConfig,
} from 'stride-ds';

// All exports
export function registerDynamicBrand(config: DynamicBrandConfig): void;
export function getDynamicBrand(brandId: string): DynamicBrandConfig | undefined;
export function getAllDynamicBrands(): DynamicBrandConfig[];
export function isDynamicBrand(brandId: string): boolean;
export function unregisterDynamicBrand(brandId: string): boolean;
export function clearAllDynamicBrands(): void;
export function configureDynamicBrandSystem(config: Partial<DynamicBrandSystemConfig>): void;
export function validateDynamicBrandConfig(config: DynamicBrandConfig): { valid: boolean; errors: string[] };

Next Steps

Build docs developers (and LLMs) love