Custom Theming

Make your Plaros embeds match your brand with custom colors, themes, and styling options.

Theme Override Options

Custom Colors

Override your tenant's default colors for specific embeds using URL parameters.

Primary Color

Controls buttons, links, and main interactive elements.

// URL parameter (without #)
?primaryColor=FF6B35

// JavaScript example
url.searchParams.set('primaryColor', '#FF6B35'.replace('#', ''));

Secondary Color

Controls accents, highlights, and secondary interactive elements.

// URL parameter (without #)
?secondaryColor=004E89

// JavaScript example
url.searchParams.set('secondaryColor', '#004E89'.replace('#', ''));

Theme Mode

Force your embed to use light or dark mode regardless of user preferences.

// Force dark mode
?themeMode=dark

// Force light mode
?themeMode=light

Complete Theming Example

const themedEmbedUrl = new URL('https://play.plaros.com/v1/embed/acme-corp/playbooks');

// Basic configuration
themedEmbedUrl.searchParams.set('zonePath', 'brand-training');

// Custom brand colors
themedEmbedUrl.searchParams.set('primaryColor', 'FF6B35');   // Orange
themedEmbedUrl.searchParams.set('secondaryColor', '004E89'); // Navy Blue

// Force dark theme
themedEmbedUrl.searchParams.set('themeMode', 'dark');

console.log(themedEmbedUrl.toString());
// https://play.plaros.com/v1/embed/acme-corp/playbooks?zonePath=brand-training&primaryColor=FF6B35&secondaryColor=004E89&themeMode=dark

Color Picker Integration

React with Color Picker

import React, { useState } from 'react';

const ThemeCustomizer = ({ tenantSlug, zonePath, onUrlChange }) => {
  const [primaryColor, setPrimaryColor] = useState('#2196F3');
  const [secondaryColor, setSecondaryColor] = useState('#9C27B0');
  const [themeMode, setThemeMode] = useState('light');
  
  const generateUrl = () => {
    const url = new URL(`https://play.plaros.com/v1/embed/${tenantSlug}/playbooks`);
    url.searchParams.set('zonePath', zonePath);
    url.searchParams.set('primaryColor', primaryColor.replace('#', ''));
    url.searchParams.set('secondaryColor', secondaryColor.replace('#', ''));
    url.searchParams.set('themeMode', themeMode);
    
    onUrlChange(url.toString());
    return url.toString();
  };
  
  return (
    <div className="theme-customizer">
      <h3>Customize Embed Theme</h3>
      
      <div className="color-controls">
        <label>
          Primary Color:
          <input
            type="color"
            value={primaryColor}
            onChange={(e) => setPrimaryColor(e.target.value)}
          />
        </label>
        
        <label>
          Secondary Color:
          <input
            type="color"
            value={secondaryColor}
            onChange={(e) => setSecondaryColor(e.target.value)}
          />
        </label>
        
        <label>
          Theme Mode:
          <select 
            value={themeMode} 
            onChange={(e) => setThemeMode(e.target.value)}
          >
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
      </div>
      
      {/* Live Preview */}
      <div className="color-preview">
        <button 
          style={{ 
            backgroundColor: primaryColor,
            color: 'white',
            border: 'none',
            padding: '8px 16px',
            borderRadius: '4px',
            marginRight: '8px'
          }}
        >
          Primary Button
        </button>
        
        <button
          style={{
            backgroundColor: 'transparent',
            color: secondaryColor,
            border: `2px solid ${secondaryColor}`,
            padding: '6px 14px',
            borderRadius: '4px',
            marginRight: '8px'
          }}
        >
          Secondary Button
        </button>
        
        <span 
          style={{ 
            backgroundColor: secondaryColor,
            color: 'white',
            padding: '4px 8px',
            borderRadius: '12px',
            fontSize: '12px'
          }}
        >
          Accent Badge
        </span>
      </div>
      
      <button onClick={generateUrl}>
        Update Embed URL
      </button>
    </div>
  );
};

Responsive Sizing

Customize how your embed appears on different screen sizes.

Standard Responsive Setup

.playbook-embed-container {
  position: relative;
  width: 100%;
  max-width: 800px; /* Adjust maximum width */
  margin: 0 auto;
}

.playbook-embed-wrapper {
  position: relative;
  padding-bottom: 75%; /* 4:3 aspect ratio */
  height: 0;
  overflow: hidden;
}

.playbook-embed-wrapper iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: 8px;
}

/* Mobile optimizations */
@media (max-width: 768px) {
  .playbook-embed-container {
    max-width: 100%;
    padding: 0 16px;
  }
  
  .playbook-embed-wrapper {
    padding-bottom: 177.78%; /* 16:9 for mobile */
  }
}

/* Tablet optimizations */
@media (min-width: 769px) and (max-width: 1024px) {
  .playbook-embed-container {
    max-width: 600px;
  }
  
  .playbook-embed-wrapper {
    padding-bottom: 133.33%; /* 3:4 for tablets */
  }
}

Custom Aspect Ratios

Different content types may benefit from different aspect ratios:

/* Square embed (1:1) */
.playbook-embed-square {
  padding-bottom: 100%;
}

/* Widescreen embed (16:9) */
.playbook-embed-widescreen {
  padding-bottom: 56.25%;
}

/* Portrait embed (3:4) */
.playbook-embed-portrait {
  padding-bottom: 133.33%;
}

/* Tall embed (9:16 - mobile-first) */
.playbook-embed-tall {
  padding-bottom: 177.78%;
}

Advanced Styling

Custom Container Styling

.playbook-embed-container {
  /* Shadow and border effects */
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  overflow: hidden;
  
  /* Background for loading states */
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  
  /* Smooth transitions */
  transition: box-shadow 0.3s ease;
}

.playbook-embed-container:hover {
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}

/* Loading state */
.playbook-embed-container::before {
  content: 'Loading interactive content...';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #666;
  font-size: 14px;
  z-index: 1;
}

.playbook-embed-container iframe {
  /* Hide loading text once iframe loads */
  position: relative;
  z-index: 2;
}

Brand Integration

.branded-embed {
  /* Company colors */
  --brand-primary: #FF6B35;
  --brand-secondary: #004E89;
  --brand-accent: #FFD23F;
  
  background: var(--brand-primary);
  padding: 20px;
  border-radius: 16px;
}

.branded-embed::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: linear-gradient(90deg, 
    var(--brand-primary) 0%, 
    var(--brand-accent) 50%, 
    var(--brand-secondary) 100%
  );
}

.branded-embed .playbook-embed-wrapper {
  background: white;
  border-radius: 8px;
  overflow: hidden;
}

CSS Variables for Dynamic Theming

Use CSS custom properties for runtime theme switching:

:root {
  --embed-primary-color: #2196F3;
  --embed-secondary-color: #9C27B0;
  --embed-border-radius: 8px;
  --embed-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

[data-theme="dark"] {
  --embed-primary-color: #90CAF9;
  --embed-secondary-color: #CE93D8;
  --embed-shadow: 0 4px 20px rgba(255, 255, 255, 0.1);
}

.dynamic-themed-embed {
  border-radius: var(--embed-border-radius);
  box-shadow: var(--embed-shadow);
}
// Switch themes dynamically
function switchEmbedTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  
  // Update embed URL if needed
  const iframe = document.querySelector('.playbook-embed iframe');
  const currentUrl = new URL(iframe.src);
  currentUrl.searchParams.set('themeMode', theme);
  iframe.src = currentUrl.toString();
}

Color Accessibility

Ensure your custom colors meet accessibility standards:

// Check color contrast ratio
function getContrastRatio(color1, color2) {
  // Implementation of WCAG contrast calculation
  // Use libraries like 'color' or 'chroma-js' for production
}

// Validate color choices
function validateColors(primaryColor, secondaryColor) {
  const contrastRatio = getContrastRatio(primaryColor, '#FFFFFF');
  
  if (contrastRatio < 4.5) {
    console.warn('Primary color may not meet WCAG AA standards');
  }
  
  return contrastRatio >= 3; // WCAG AA Large text minimum
}

Theme Testing

Test your custom themes across different scenarios:

// Test configuration
const themeTestCases = [
  {
    name: 'Corporate Blue',
    primaryColor: '0066CC',
    secondaryColor: '6699FF',
    themeMode: 'light'
  },
  {
    name: 'Dark Mode Orange',
    primaryColor: 'FF6B35',
    secondaryColor: 'FFD23F',
    themeMode: 'dark'
  },
  {
    name: 'High Contrast',
    primaryColor: '000000',
    secondaryColor: 'FFFFFF',
    themeMode: 'light'
  }
];

// Generate test URLs
themeTestCases.forEach(theme => {
  const url = new URL('https://play.plaros.com/v1/embed/test-tenant/playbooks');
  url.searchParams.set('zonePath', 'theme-test');
  url.searchParams.set('primaryColor', theme.primaryColor);
  url.searchParams.set('secondaryColor', theme.secondaryColor);
  url.searchParams.set('themeMode', theme.themeMode);
  
  console.log(`${theme.name}: ${url.toString()}`);
});

Next Steps

  • See Integration Examples for framework-specific theming implementations

  • Learn about Configuration Options for additional customization

  • Check Troubleshooting for common theming issues