Skip to main content

Theme Switching

Stride’s brand system supports seamless runtime switching between brands with smooth transitions, automatic persistence, and React hooks for integration.

Core Function: applyBrandTheme

The applyBrandTheme() function applies a brand theme at runtime, handling both static and dynamic brands automatically. Function Signature (from src/lib/brands.ts:547)
export const applyBrandTheme = (brandId: string): void

Basic Usage

import { applyBrandTheme } from 'stride-ds';

// Apply a static brand
applyBrandTheme('stride');
applyBrandTheme('coral');
applyBrandTheme('forest');

// Apply a dynamic brand (registered with registerDynamicBrand)
applyBrandTheme('client-abc');
The function automatically:
  • Detects if the brand is static or dynamic
  • Removes previous brand classes
  • Applies new brand classes
  • Updates CSS variables for dynamic brands
  • Persists choice to localStorage
  • Triggers smooth transitions

How It Works

From src/lib/brands.ts:547-590, here’s what happens when you call applyBrandTheme:
export const applyBrandTheme = (brandId: string): void => {
  // 1. Check if it's a dynamic brand
  if (isDynamicBrand(brandId)) {
    applyDynamicBrandTheme(brandId);
    return;
  }

  // 2. Find the static brand
  const brand = getBrandById(brandId);
  if (!brand) {
    console.warn(`❌ Brand "${brandId}" not found`);
    return;
  }

  // 3. Add transition class if enabled
  if (dynamicBrandSystemConfig.enableTransitions) {
    document.documentElement.classList.add('brand-switching');
  }

  // 4. Clean up existing brand classes
  cleanupExistingBrandClasses();
  
  // 5. Apply the new brand class
  document.documentElement.classList.add(`brand-${brand.id}`);
  
  // 6. Save to localStorage
  if (dynamicBrandSystemConfig.enableLocalStorage) {
    localStorage.setItem('stride-brand', brandId);
  }

  // 7. Remove transition class after duration
  if (dynamicBrandSystemConfig.enableTransitions) {
    setTimeout(() => {
      document.documentElement.classList.remove('brand-switching');
    }, dynamicBrandSystemConfig.transitionDuration);
  }
};

React Hook: useBrand

The useBrand hook provides a React-friendly interface for brand management. Hook Signature (from src/lib/useBrand.ts:21-38)
interface UseBrandReturn {
  currentBrand: string;
  currentBrandType: 'static' | 'dynamic';
  availableBrands: BrandTheme[];
  dynamicBrands: DynamicBrandConfig[];
  allBrands: Array<BrandTheme | DynamicBrandConfig>;
  setBrand: (brandId: string) => void;
  getCurrentBrandTheme: () => BrandTheme | DynamicBrandConfig | undefined;
  
  // Dynamic brand management
  registerDynamicBrand: (config: DynamicBrandConfig) => void;
  unregisterDynamicBrand: (brandId: string) => boolean;
  isDynamicBrand: (brandId: string) => boolean;
  clearAllDynamicBrands: () => void;
  
  // System configuration
  configureDynamicSystem: (config: Partial<DynamicBrandSystemConfig>) => void;
}

export const useBrand = (): UseBrandReturn

Basic React Example

import { useBrand } from 'stride-ds';

function BrandSwitcher() {
  const { currentBrand, setBrand, availableBrands } = useBrand();
  
  return (
    <div>
      <p>Current: {currentBrand}</p>
      
      {availableBrands.map(brand => (
        <button
          key={brand.id}
          onClick={() => setBrand(brand.id)}
          disabled={currentBrand === brand.id}
        >
          {brand.name}
        </button>
      ))}
    </div>
  );
}

Advanced React Example

From src/stories/DynamicBrandSystem.stories.tsx:187-197:
import { useBrand } from 'stride-ds';

function BrandManager() {
  const {
    currentBrand,
    currentBrandType,
    setBrand,
    availableBrands,
    dynamicBrands,
    registerDynamicBrand,
    unregisterDynamicBrand,
  } = useBrand();
  
  const handleCreateBrand = () => {
    registerDynamicBrand({
      id: 'new-client',
      name: 'New Client',
      tokens: {
        core: {
          primary: { 500: '#9333ea' }
        }
      }
    });
    
    // Switch to the new brand
    setBrand('new-client');
  };
  
  const handleDeleteBrand = (brandId: string) => {
    const success = unregisterDynamicBrand(brandId);
    if (success) {
      console.log(`Deleted ${brandId}`);
    }
  };
  
  return (
    <div>
      <h2>Current Brand: {currentBrand} ({currentBrandType})</h2>
      
      <section>
        <h3>Static Brands</h3>
        {availableBrands.map(brand => (
          <button
            key={brand.id}
            onClick={() => setBrand(brand.id)}
            className={currentBrand === brand.id ? 'active' : ''}
          >
            {brand.name}
          </button>
        ))}
      </section>
      
      <section>
        <h3>Dynamic Brands</h3>
        {dynamicBrands.map(brand => (
          <div key={brand.id}>
            <button onClick={() => setBrand(brand.id)}>
              {brand.name}
            </button>
            <button onClick={() => handleDeleteBrand(brand.id)}>
              Delete
            </button>
          </div>
        ))}
        
        <button onClick={handleCreateBrand}>
          Create New Brand
        </button>
      </section>
    </div>
  );
}

Getting Current Brand

Retrieve the currently active brand programmatically. Function Signature (from src/lib/brands.ts:593)
export const getCurrentBrand = (): string
Usage:
import { getCurrentBrand, getCurrentBrandType } from 'stride-ds';

const activeBrand = getCurrentBrand();
console.log('Current brand:', activeBrand); // 'stride', 'client-abc', etc.

const brandType = getCurrentBrandType();
console.log('Type:', brandType); // 'static' or 'dynamic'
In React:
import { useBrand } from 'stride-ds';

function CurrentBrandDisplay() {
  const { currentBrand, currentBrandType, getCurrentBrandTheme } = useBrand();
  const theme = getCurrentBrandTheme();
  
  return (
    <div>
      <p>Brand ID: {currentBrand}</p>
      <p>Type: {currentBrandType}</p>
      <p>Name: {theme?.name}</p>
      <p>Description: {theme?.description}</p>
    </div>
  );
}

localStorage Persistence

Brands are automatically persisted to localStorage and restored on page load.

How Persistence Works

From src/lib/brands.ts:276-283 and src/lib/brands.ts:578-580:
// When registering a dynamic brand
if (dynamicBrandSystemConfig.enableLocalStorage) {
  localStorage.setItem(`stride-dynamic-brand:${config.id}`, JSON.stringify(finalConfig));
}

// When applying a brand
if (dynamicBrandSystemConfig.enableLocalStorage) {
  // Static brand
  localStorage.setItem('stride-brand', brandId);
  
  // Dynamic brand (stored with prefix)
  localStorage.setItem('stride-brand', `dynamic:${brandId}`);
}

Restoring Brands on Load

From src/lib/brands.ts:504-544:
export const restoreDynamicBrandsFromStorage = (): void => {
  // 1. Restore all registered dynamic brands from localStorage
  Object.keys(localStorage).forEach(key => {
    if (key.startsWith('stride-dynamic-brand:')) {
      const brandId = key.replace('stride-dynamic-brand:', '');
      const storedConfig = localStorage.getItem(key);
      
      if (storedConfig) {
        const config: DynamicBrandConfig = JSON.parse(storedConfig);
        registerDynamicBrand(config);
      }
    }
  });

  // 2. Apply the active brand
  const currentBrand = localStorage.getItem('stride-brand');
  if (currentBrand?.startsWith('dynamic:')) {
    const brandId = currentBrand.replace('dynamic:', '');
    applyDynamicBrandTheme(brandId);
  }
};

Initialization in App

From src/lib/brands.ts:615-622:
export const initializeBrand = (): void => {
  // Restore dynamic brands first
  restoreDynamicBrandsFromStorage();
  
  // Then apply the current brand
  const savedBrand = getCurrentBrand();
  applyBrandTheme(savedBrand);
};
Usage in your app:
// In your root layout or App component
import { useEffect } from 'react';
import { initializeBrand } from 'stride-ds';

function App() {
  useEffect(() => {
    initializeBrand();
  }, []);
  
  return <YourApp />;
}

Disabling Persistence

You can disable localStorage persistence if needed:
import { configureDynamicBrandSystem } from 'stride-ds';

configureDynamicBrandSystem({
  enableLocalStorage: false  // Disable persistence
});

Transition Configuration

Control how brand switches are animated.

Global Transition Configuration

Config Interface (from src/lib/brands.ts:173-179)
interface DynamicBrandSystemConfig {
  defaultFallbackBrand: string;    // Default fallback brand
  enableLocalStorage: boolean;     // Enable persistence
  enableTransitions: boolean;      // Enable animations
  transitionDuration: number;      // Duration in milliseconds
}
Default configuration (from src/lib/brands.ts:232-237):
let dynamicBrandSystemConfig: DynamicBrandSystemConfig = {
  defaultFallbackBrand: 'stride',
  enableLocalStorage: true,
  enableTransitions: true,
  transitionDuration: 50,  // 50ms by default
};

Configuring Transitions

Function Signature (from src/lib/brands.ts:243)
export const configureDynamicBrandSystem = (
  config: Partial<DynamicBrandSystemConfig>
): void
Examples:
import { configureDynamicBrandSystem } from 'stride-ds';

// Slower transitions
configureDynamicBrandSystem({
  enableTransitions: true,
  transitionDuration: 200  // 200ms
});

// Disable transitions
configureDynamicBrandSystem({
  enableTransitions: false
});

// Configure all settings
configureDynamicBrandSystem({
  defaultFallbackBrand: 'forest',
  enableLocalStorage: true,
  enableTransitions: true,
  transitionDuration: 100
});
In React:
import { useBrand } from 'stride-ds';

function BrandSettings() {
  const { configureDynamicSystem } = useBrand();
  
  const handleFastTransitions = () => {
    configureDynamicSystem({
      transitionDuration: 50
    });
  };
  
  const handleSlowTransitions = () => {
    configureDynamicSystem({
      transitionDuration: 300
    });
  };
  
  const handleDisableTransitions = () => {
    configureDynamicSystem({
      enableTransitions: false
    });
  };
  
  return (
    <div>
      <button onClick={handleFastTransitions}>Fast (50ms)</button>
      <button onClick={handleSlowTransitions}>Slow (300ms)</button>
      <button onClick={handleDisableTransitions}>Disable</button>
    </div>
  );
}

How Transitions Work

From src/app/brands.css:744-756:
/* Global transition rules */
:root {
  --brand-transition: all 200ms ease-in-out;
}

* {
  transition: var(--brand-transition);
  transition-property: background-color, border-color, color, box-shadow;
}

/* Disable transitions during brand switch */
.brand-switching * {
  transition: none !important;
}
The system:
  1. Adds .brand-switching class to <html> before switching
  2. Disables all transitions temporarily
  3. Changes brand classes and CSS variables
  4. Removes .brand-switching after configured duration
  5. Transitions resume, animating to new colors
This prevents a “flash” effect and ensures smooth color transitions.

Available Brands Utilities

Get lists of available brands.

Get All Brands

Function Signatures (from src/lib/brands.ts)
// Get static brands only (line 213)
export const availableBrands: BrandTheme[] = [
  strideBrand,
  coralBrand,
  forestBrand,
  runswapBrand,
  acmeBrand,
];

// Get dynamic brands only (line 294)
export const getAllDynamicBrands = (): DynamicBrandConfig[]

// Get all brands (static + dynamic) (line 625)
export const getAllBrands = (): Array<BrandTheme | DynamicBrandConfig>

// Get specific brand (line 221)
export const getBrandById = (brandId: string): BrandTheme | undefined

// Get specific dynamic brand (line 289)
export const getDynamicBrand = (brandId: string): DynamicBrandConfig | undefined

// Check if brand is dynamic (line 299)
export const isDynamicBrand = (brandId: string): boolean
Usage:
import {
  availableBrands,
  getAllDynamicBrands,
  getAllBrands,
  getBrandById,
  isDynamicBrand
} from 'stride-ds';

// List all static brands
console.log('Static brands:', availableBrands);
// Output: [{ id: 'stride', name: 'Stride', ... }, ...]

// List all dynamic brands
const dynamicBrands = getAllDynamicBrands();
console.log('Dynamic brands:', dynamicBrands);

// List everything
const allBrands = getAllBrands();
console.log('All brands:', allBrands);

// Get specific brand
const stride = getBrandById('stride');
console.log('Stride:', stride);

// Check if brand is dynamic
const isClientDynamic = isDynamicBrand('client-abc');
console.log('Is dynamic:', isClientDynamic);

Advanced: Dynamic Brand Switching

For dynamic brands, the switching process includes generating CSS on the fly. From src/lib/brands.ts:330-377:
export const applyDynamicBrandTheme = (brandId: string): void => {
  const dynamicBrand = getDynamicBrand(brandId);
  if (!dynamicBrand) {
    console.error(`❌ Dynamic brand "${brandId}" not found`);
    return;
  }

  const root = document.documentElement;

  // 1. Add transition class
  if (dynamicBrandSystemConfig.enableTransitions) {
    root.classList.add('brand-switching');
  }

  // 2. Clean up existing brand classes
  cleanupExistingBrandClasses();

  // 3. Apply fallback brand first (for undefined tokens)
  const fallbackBrand = dynamicBrand.fallback?.brand || dynamicBrandSystemConfig.defaultFallbackBrand;
  if (fallbackBrand) {
    root.classList.add(`brand-${fallbackBrand}`);
  }

  // 4. Apply dynamic brand class
  root.classList.add(`brand-dynamic-${brandId}`);

  // 5. Generate and inject CSS variables
  updateDynamicBrandStyles(dynamicBrand);

  // 6. Save to localStorage
  if (dynamicBrandSystemConfig.enableLocalStorage) {
    localStorage.setItem('stride-brand', `dynamic:${brandId}`);
  }

  // 7. Remove transition class
  if (dynamicBrandSystemConfig.enableTransitions) {
    setTimeout(() => {
      root.classList.remove('brand-switching');
    }, dynamicBrandSystemConfig.transitionDuration);
  }
};
The system:
  1. Applies fallback brand class first (e.g., .brand-stride)
  2. Applies dynamic brand class (e.g., .brand-dynamic-client-abc)
  3. Generates CSS with defined tokens
  4. Undefined tokens inherit from fallback brand

Unregistering Brands

Remove dynamic brands when no longer needed. Function Signature (from src/lib/brands.ts:304)
export const unregisterDynamicBrand = (brandId: string): boolean
Usage:
import { unregisterDynamicBrand, getCurrentBrand } from 'stride-ds';

const success = unregisterDynamicBrand('client-abc');

if (success) {
  console.log('Brand removed successfully');
  // If it was the active brand, system reverts to default
} else {
  console.log('Brand not found');
}
Clear all dynamic brands:
import { clearAllDynamicBrands } from 'stride-ds';

clearAllDynamicBrands();
// Removes all dynamic brands and reverts to default

Complete Example: Brand Switcher Component

Here’s a production-ready brand switcher:
import React from 'react';
import { useBrand } from 'stride-ds';
import type { BrandTheme, DynamicBrandConfig } from 'stride-ds';

export function BrandSwitcher() {
  const {
    currentBrand,
    currentBrandType,
    setBrand,
    availableBrands,
    dynamicBrands,
    allBrands,
  } = useBrand();
  
  const handleBrandChange = (brandId: string) => {
    setBrand(brandId);
  };
  
  return (
    <div className="brand-switcher">
      <div className="current-brand">
        <span className="label">Current Brand:</span>
        <span className="value">
          {currentBrand} ({currentBrandType})
        </span>
      </div>
      
      <div className="brand-grid">
        {allBrands.map((brand) => {
          const isActive = brand.id === currentBrand;
          const isDynamic = 'tokens' in brand;
          
          return (
            <button
              key={brand.id}
              onClick={() => handleBrandChange(brand.id)}
              className={`
                brand-option
                ${isActive ? 'active' : ''}
                ${isDynamic ? 'dynamic' : 'static'}
              `}
              disabled={isActive}
            >
              <div className="brand-name">{brand.name}</div>
              <div className="brand-type">
                {isDynamic ? 'Dynamic' : 'Static'}
              </div>
              {brand.description && (
                <div className="brand-description">
                  {brand.description}
                </div>
              )}
            </button>
          );
        })}
      </div>
      
      <div className="brand-counts">
        <span>{availableBrands.length} static brands</span>
        <span>{dynamicBrands.length} dynamic brands</span>
      </div>
    </div>
  );
}

Best Practices

  1. Initialize on app load
    useEffect(() => {
      initializeBrand();
    }, []);
    
  2. Use the useBrand hook in React
    const { setBrand } = useBrand();
    // Better than calling applyBrandTheme directly
    
  3. Validate brand IDs before switching
    const brand = getBrandById(brandId) || getDynamicBrand(brandId);
    if (brand) {
      setBrand(brandId);
    }
    
  4. Configure transitions once at app start
    configureDynamicBrandSystem({
      transitionDuration: 100,
      enableTransitions: true
    });
    
  5. Handle brand switching in loading states
    const [switching, setSwitching] = useState(false);
    
    const handleSwitch = async (brandId: string) => {
      setSwitching(true);
      setBrand(brandId);
      setTimeout(() => setSwitching(false), transitionDuration);
    };
    

Next Steps

Build docs developers (and LLMs) love