BrainDrive Plugin Theming Guide
For Plugin AI Coding Agent Developers
This guide explains how to properly implement dark/light mode theming in BrainDrive plugins to ensure compliance with the host system's theme switching.
Quick Answer
❌ Can I use Tailwind CSS dark mode?
NO - Tailwind CSS is not installed in BrainDrive and the dark: variant will not work.
✅ What should I use instead?
Use CSS Custom Properties (CSS Variables) with the .dark-theme class selector. This is the proven, working pattern used by all existing BrainDrive plugins.
How BrainDrive's Theme System Works
1. Theme Service Controls Everything
The host system uses a singleton ThemeService located at:
frontend/src/services/themeService.ts
What it does:
- Controls theme switching across the entire application
- Applies CSS classes to the DOM
- Notifies all listeners when theme changes
- Plugins don't control themes - they react to theme changes
2. DOM Changes on Theme Switch
When the user toggles dark mode, the ThemeService modifies the DOM:
// Light mode (default)
<html> <!-- NO .dark class -->
<body> <!-- NO .dark-scrollbars class -->
// Dark mode
<html class="dark"> <!-- .dark class ADDED -->
<body class="dark-scrollbars"> <!-- .dark-scrollbars class ADDED -->
Key Classes:
.dark→ Applied todocument.documentElement(<html>).dark-scrollbars→ Applied todocument.body
The Correct Way: CSS Custom Properties
Step 1: Define Your Theme Variables
Create a CSS file for your plugin (e.g., MyPlugin.css):
/* Light theme variables (default) */
:root {
--my-plugin-bg: #ffffff;
--my-plugin-text: #333333;
--my-plugin-border: #e0e0e0;
--my-plugin-card-bg: #f5f5f5;
--my-plugin-shadow: rgba(0, 0, 0, 0.1);
--my-plugin-hover: rgba(0, 0, 0, 0.05);
}
/* Dark theme overrides */
.dark-theme {
--my-plugin-bg: #121a28;
--my-plugin-text: #e0e0e0;
--my-plugin-border: rgba(255, 255, 255, 0.1);
--my-plugin-card-bg: #1e1e1e;
--my-plugin-shadow: rgba(0, 0, 0, 0.3);
--my-plugin-hover: rgba(255, 255, 255, 0.1);
}
Step 2: Use Variables in Your Styles
.my-plugin-container {
background-color: var(--my-plugin-bg);
color: var(--my-plugin-text);
border: 1px solid var(--my-plugin-border);
transition: background-color 0.3s ease, color 0.3s ease;
}
.my-plugin-card {
background-color: var(--my-plugin-card-bg);
box-shadow: 0 2px 4px var(--my-plugin-shadow);
}
.my-plugin-card:hover {
background-color: var(--my-plugin-hover);
}
Step 3: React Integration (Functional Component)
import React, { useState, useEffect } from 'react';
import './MyPlugin.css';
interface MyPluginProps {
services?: {
theme?: {
getCurrentTheme: () => 'light' | 'dark';
addThemeChangeListener: (listener: (theme: string) => void) => void;
removeThemeChangeListener: (listener: (theme: string) => void) => void;
};
};
}
const MyPlugin: React.FC<MyPluginProps> = ({ services }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
// Get initial theme
if (services?.theme) {
const currentTheme = services.theme.getCurrentTheme();
setTheme(currentTheme);
// Listen for theme changes
const handleThemeChange = (newTheme: string) => {
setTheme(newTheme as 'light' | 'dark');
};
services.theme.addThemeChangeListener(handleThemeChange);
// Cleanup listener on unmount
return () => {
services.theme.removeThemeChangeListener(handleThemeChange);
};
}
}, [services]);
return (
<div className={`my-plugin-container ${theme === 'dark' ? 'dark-theme' : ''}`}>
<h2>My Plugin</h2>
<div className="my-plugin-card">
This card adapts to light/dark theme automatically!
</div>
</div>
);
};
export default MyPlugin;
Why This Approach Works
✅ Advantages
- Host System Compliant - Follows established patterns
- No Build Conflicts - Works with BrainDrive's Material-UI setup
- Automatic Theme Sync - Responds instantly to theme changes
- Smooth Transitions - CSS transitions work perfectly
- Maintainable - Clear separation of light/dark values
- Proven Pattern - Used by all existing plugins
❌ Why Tailwind Won't Work
- Not Installed - BrainDrive uses Material-UI with Emotion (CSS-in-JS)
- No Configuration - No
tailwind.config.jsexists - Wrong Selector - Tailwind's
dark:needsclass="dark"on a parent - Build Conflicts - Would conflict with Material-UI's styling system
- Bundle Size - Adds unnecessary bloat
Alternative: React Hook Pattern
If you prefer a custom hook for theme integration:
// useTheme.ts
import { useState, useEffect } from 'react';
interface ThemeService {
getCurrentTheme: () => 'light' | 'dark';
addThemeChangeListener: (listener: (theme: string) => void) => void;
removeThemeChangeListener: (listener: (theme: string) => void) => void;
}
export const useTheme = (themeService?: ThemeService) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
if (!themeService) return;
const currentTheme = themeService.getCurrentTheme();
setTheme(currentTheme);
const listener = (newTheme: string) => setTheme(newTheme as 'light' | 'dark');
themeService.addThemeChangeListener(listener);
return () => themeService.removeThemeChangeListener(listener);
}, [themeService]);
return theme;
};
Usage:
const MyPlugin: React.FC<MyPluginProps> = ({ services }) => {
const theme = useTheme(services?.theme);
return (
<div className={theme === 'dark' ? 'dark-theme' : ''}>
{/* Your content */}
</div>
);
};
Best Practices
DO ✅
- Use CSS Custom Properties for all theme-dependent colors
- Subscribe to theme changes via
addThemeChangeListener - Apply
.dark-themeclass to your plugin's root element - Add smooth transitions for better UX
- Test in both modes before deployment
- Use semantic variable names (e.g.,
--text-primarynot--color-1) - Namespace your variables (e.g.,
--my-plugin-bgnot--bg)
DON'T ❌
- Don't use Tailwind CSS - It's not supported
- Don't hardcode colors - Always use variables
- Don't import Material-UI in plugins - Creates conflicts
- Don't rely on
.darkclass on<html>- Use.dark-themeon your component - Don't forget to cleanup listeners - Memory leaks!
- Don't use inline styles for theme colors - Defeats the purpose
Example: Complete Plugin with Theming
// MyPlugin.tsx
import React, { useState, useEffect } from 'react';
import './MyPlugin.css';
interface MyPluginProps {
services?: {
theme?: {
getCurrentTheme: () => 'light' | 'dark';
addThemeChangeListener: (listener: (theme: string) => void) => void;
removeThemeChangeListener: (listener: (theme: string) => void) => void;
};
};
}
const MyPlugin: React.FC<MyPluginProps> = ({ services }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [data, setData] = useState<string[]>([]);
useEffect(() => {
// Theme integration
if (services?.theme) {
const currentTheme = services.theme.getCurrentTheme();
setTheme(currentTheme);
const handleThemeChange = (newTheme: string) => {
setTheme(newTheme as 'light' | 'dark');
};
services.theme.addThemeChangeListener(handleThemeChange);
return () => services.theme.removeThemeChangeListener(handleThemeChange);
}
}, [services]);
return (
<div className={`my-plugin ${theme === 'dark' ? 'dark-theme' : ''}`}>
<div className="my-plugin-header">
<h2>My Plugin</h2>
</div>
<div className="my-plugin-content">
<div className="my-plugin-card">
Current theme: {theme}
</div>
</div>
</div>
);
};
export default MyPlugin;
/* MyPlugin.css */
/* Light theme (default) */
:root {
--my-plugin-bg: #ffffff;
--my-plugin-text: #333333;
--my-plugin-border: #e0e0e0;
--my-plugin-header-bg: #f5f5f5;
--my-plugin-card-bg: #ffffff;
--my-plugin-shadow: rgba(0, 0, 0, 0.1);
}
/* Dark theme */
.dark-theme {
--my-plugin-bg: #121a28;
--my-plugin-text: #e0e0e0;
--my-plugin-border: rgba(255, 255, 255, 0.1);
--my-plugin-header-bg: #1a2332;
--my-plugin-card-bg: #1e1e1e;
--my-plugin-shadow: rgba(0, 0, 0, 0.3);
}
/* Component styles using variables */
.my-plugin {
background-color: var(--my-plugin-bg);
color: var(--my-plugin-text);
border: 1px solid var(--my-plugin-border);
border-radius: 8px;
padding: 16px;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.my-plugin-header {
background-color: var(--my-plugin-header-bg);
padding: 12px;
border-radius: 6px;
margin-bottom: 16px;
transition: background-color 0.3s ease;
}
.my-plugin-header h2 {
margin: 0;
color: var(--my-plugin-text);
}
.my-plugin-card {
background-color: var(--my-plugin-card-bg);
border: 1px solid var(--my-plugin-border);
border-radius: 6px;
padding: 16px;
box-shadow: 0 2px 4px var(--my-plugin-shadow);
transition: all 0.3s ease;
}
.my-plugin-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--my-plugin-shadow);
}
Common Color Variables Pattern
Use this as a starting template:
:root {
/* Backgrounds */
--plugin-bg-primary: #ffffff;
--plugin-bg-secondary: #f5f5f5;
--plugin-bg-tertiary: #e0e0e0;
/* Text */
--plugin-text-primary: #333333;
--plugin-text-secondary: #666666;
--plugin-text-muted: #999999;
/* Borders */
--plugin-border-light: rgba(0, 0, 0, 0.1);
--plugin-border-medium: rgba(0, 0, 0, 0.2);
--plugin-border-heavy: rgba(0, 0, 0, 0.3);
/* Interactive */
--plugin-hover-bg: rgba(0, 0, 0, 0.05);
--plugin-active-bg: rgba(0, 0, 0, 0.1);
/* Shadows */
--plugin-shadow-sm: rgba(0, 0, 0, 0.1);
--plugin-shadow-md: rgba(0, 0, 0, 0.15);
--plugin-shadow-lg: rgba(0, 0, 0, 0.2);
}
.dark-theme {
/* Backgrounds */
--plugin-bg-primary: #121a28;
--plugin-bg-secondary: #1e1e1e;
--plugin-bg-tertiary: #2a2a2a;
/* Text */
--plugin-text-primary: #e0e0e0;
--plugin-text-secondary: #b0b0b0;
--plugin-text-muted: #808080;
/* Borders */
--plugin-border-light: rgba(255, 255, 255, 0.1);
--plugin-border-medium: rgba(255, 255, 255, 0.2);
--plugin-border-heavy: rgba(255, 255, 255, 0.3);
/* Interactive */
--plugin-hover-bg: rgba(255, 255, 255, 0.1);
--plugin-active-bg: rgba(255, 255, 255, 0.15);
/* Shadows */
--plugin-shadow-sm: rgba(0, 0, 0, 0.3);
--plugin-shadow-md: rgba(0, 0, 0, 0.4);
--plugin-shadow-lg: rgba(0, 0, 0, 0.5);
}
Testing Your Theme Implementation
Checklist ✅
- Light mode displays correctly with good contrast
- Dark mode displays correctly with good contrast
- Theme switches instantly when user toggles
- No flash of unstyled content (FOUC)
- Smooth transitions between themes
- All text is readable in both modes
- Borders and shadows are visible in both modes
- Hover states work in both modes
- No console errors about theme service
- Theme persists across page reloads
Reference Examples
Working Plugins Using This Pattern:
-
BrainDriveChat v1.0.11
- File:
backend/plugins/shared/BrainDriveChat/v1.0.11/src/BrainDriveChat.css - Pattern: CSS variables + class-based theming
- File:
-
BrainDriveSettings v1.0.2
- File:
backend/plugins/shared/BrainDriveSettings/v1.0.2/src/ComponentTheme.tsx - Pattern: React hook + theme service integration
- File:
-
CollectionViewer v1.0.0 (New)
- File:
backend/plugins/shared/CollectionViewer/v1.0.0/src/CollectionViewer.css - Pattern: CSS variables with functional component
- File:
Summary
For Plugin AI Coding Agents:
You MUST use CSS Custom Properties with the .dark-theme class selector. This is not optional - it's the only supported method in BrainDrive.
Do NOT attempt to:
- Install Tailwind CSS
- Use Tailwind's
dark:variant - Configure PostCSS for Tailwind
- Import Material-UI components
The host system controls theming:
- You don't set themes
- You react to theme changes
- Subscribe to theme service events
- Apply
.dark-themeclass based on current theme
This ensures your plugins are fully compliant with BrainDrive's theme system and will work seamlessly with host-controlled theme switching.
Need Help?
If you encounter issues:
- Verify theme service is available:
console.log(services?.theme) - Check if listener is being called: Add logs in theme change handler
- Inspect DOM: Verify
.dark-themeclass is being applied - Check CSS specificity: Ensure variables are properly scoped
- Review existing plugins for working examples
File Locations:
- Theme Service:
frontend/src/services/themeService.ts - Example CSS:
backend/plugins/shared/CollectionViewer/v1.0.0/src/CollectionViewer.css - Example Component:
backend/plugins/shared/CollectionViewer/v1.0.0/src/CollectionViewer.tsx