Theme Customization
StudioBrain uses a semantic surface system for all visual styling. The system provides 12 surfaces, each with 7 coordinated CSS properties, totaling 84 CSS custom properties that users can customize through the Settings UI. Every component in the application — including plugins — uses these surfaces to ensure consistent, user-customizable theming.
Available Surfaces
Surfaces are organized into three categories based on their purpose.
Layout Surfaces (Neutral, Hierarchical)
These surfaces form a visual depth hierarchy: base sits behind elevated, which sits behind overlay.
| Surface | Purpose | Light Default | Dark Default |
|---|---|---|---|
base | Main page background, content areas | White bg, dark text | Dark bg, light text |
elevated | Cards, panels, sidebars, sections | Light gray bg, dark text | Slightly lighter bg, light text |
overlay | Modals, dropdowns, popovers, tooltips | White bg with shadow, dark text | Dark bg with shadow, light text |
Brand Surfaces (Colorful, Interactive)
Brand surfaces use solid colored backgrounds with white text. Use them for buttons, badges, CTAs, and interactive elements.
| Surface | Purpose | Light Default | Dark Default |
|---|---|---|---|
primary | Main brand actions, CTAs, active states | Blue bg, white text | Blue bg, white text |
secondary | Secondary actions, alternative choices | Purple bg, white text | Purple bg, white text |
accent | Highlights, badges, special emphasis, AI features | Green bg, white text | Green bg, white text |
Status Surfaces (Semantic Meaning)
Status surfaces use tinted backgrounds with dark text (light mode) or muted backgrounds with light text (dark mode). Use them for alerts, notices, and state indicators.
| Surface | Purpose | Light Default | Dark Default |
|---|---|---|---|
success | Success confirmations, positive states | Green-tinted bg, dark green text | Dark green bg, light green text |
warning | Caution alerts, pending states | Yellow-tinted bg, dark yellow text | Dark yellow bg, light yellow text |
error | Error alerts, destructive states, delete actions | Red-tinted bg, dark red text | Dark red bg, light red text |
info | Informational notices, tips, help text | Blue-tinted bg, dark blue text | Dark blue bg, light blue text |
Specialized Surfaces
| Surface | Purpose | Description |
|---|---|---|
navigation | Sidebar, top nav, nav items | Coordinates with the layout surfaces but independently configurable |
input | Form inputs, text fields, selects, textareas | Provides distinct styling for form elements with focus states |
Surface Properties
Each surface has 7 coordinated properties. The CSS variable naming convention is:
--surface-{name}-bg
--surface-{name}-text
--surface-{name}-text-secondary
--surface-{name}-border
--surface-{name}-hover
--surface-{name}-active
--surface-{name}-shadow| Property | Variable Suffix | Purpose |
|---|---|---|
| Background | -bg | Main background color |
| Text | -text | Primary text color on this surface |
| Text Secondary | -text-secondary | Muted/secondary text on this surface |
| Border | -border | Border color for elements on this surface |
| Hover | -hover | Background on hover state |
| Active | -active | Background on active/pressed state |
| Shadow | -shadow | Box shadow value |
For example, the elevated surface exposes:
--surface-elevated-bg--surface-elevated-text--surface-elevated-text-secondary--surface-elevated-border--surface-elevated-hover--surface-elevated-active--surface-elevated-shadow
CSS Class Reference
Tailwind utility classes are generated for every surface property in globals.css.
Background
bg-surface-{name} Background color
hover:bg-surface-{name}-hover Hover state background
active:bg-surface-{name}-active Active/pressed state backgroundText
text-surface-{name}-text Primary text on this surface
text-surface-{name}-text-secondary Secondary/muted text on this surfaceBorder
border-surface-{name}-border Border color for this surfaceShadow
shadow-surface-{name} Box shadow for this surfaceBrand Color as Text
When you need brand-colored text on a neutral background (not on a brand-colored background), use the -color variant:
text-surface-primary-color Primary brand color as text
text-surface-secondary-color Secondary brand color as text
text-surface-accent-color Accent brand color as textThese classes use var(--surface-primary-bg) (etc.) as the text color. Use them for links, headings, or highlighted text on base or elevated backgrounds.
Important: text-surface-primary-text is white (for text ON a primary background). text-surface-primary-color is the brand blue (for colored text on neutral backgrounds). Do not confuse them.
Ring (Focus)
ring-surface-primary Focus ring using primary color
focus:ring-surface-primary-border Focus ring using primary borderSurface Consistency Rule
Text classes must match their container’s surface. This is the most important rule for theme correctness.
// Correct: elevated container with elevated text
<div className="bg-surface-elevated text-surface-elevated-text">
<p className="text-surface-elevated-text-secondary">Muted text</p>
</div>
// Correct: base container with base text
<div className="bg-surface-base text-surface-base-text">
<p className="text-surface-base-text-secondary">Muted text</p>
</div>
// Wrong: mismatched surfaces break with custom themes
<div className="bg-surface-base text-surface-elevated-text">
{/* This will look wrong if the user has customized these surfaces differently */}
</div>Exception: Sub-elements within a surface CAN use a different surface for visual depth. For example, an input surface element inside an elevated panel is correct. But the text inside the input must match the input surface, not the parent.
Global Text Color Cascade
Base surface text colors cascade to elevated and overlay surfaces automatically. When a user changes “Base Surface - Primary Text” in the theme editor, the new color propagates to elevated and overlay text. This enables the “Global Text Color System” in the Settings UI.
For Plugin Developers
Plugin theme compliance is mandatory. Plugins that use hardcoded colors break when users customize their themes. See the Plugin Development page for the complete guide on theme compliance.
Key points:
- Plugins receive
theme-changemessages when the user toggles light/dark mode. - Plugin iframes do not inherit host CSS variables. You must define your own variables that mirror the surface naming convention.
- Never use hardcoded hex values, Tailwind color utilities (
bg-blue-500), or any color that does not come from a CSS variable.
For Code Contributors
JavaScript Helpers
The src/lib/theme-classes.ts module provides pre-composed class strings for common patterns.
surfaces — Static Surface Classes
Combines background, text, and border for each surface:
import { surfaces } from '@/lib/theme-classes';
// surfaces.elevated = 'bg-surface-elevated text-surface-elevated-text
// border-surface-elevated-border shadow-surface-elevated'
<div className={surfaces.elevated}>Card content</div>
// surfaces.success = 'bg-surface-success text-surface-success-text
// border-surface-success-border'
<div className={surfaces.success}>Success message</div>Available: surfaces.base, surfaces.elevated, surfaces.overlay, surfaces.primary, surfaces.secondary, surfaces.accent, surfaces.success, surfaces.warning, surfaces.error, surfaces.info, surfaces.navigation, surfaces.input.
surfacesInteractive — Interactive Surface Classes
Adds hover and active states with transitions:
import { surfacesInteractive } from '@/lib/theme-classes';
// surfacesInteractive.primary = 'bg-surface-primary text-surface-primary-text
// border-surface-primary-border hover:bg-surface-primary-hover
// active:bg-surface-primary-active transition-colors'
<button className={surfacesInteractive.primary}>Click me</button>Available: surfacesInteractive.primary, .secondary, .accent, .base, .elevated, .navigation.
surfaceComponents — Pre-Composed Components
Ready-to-use class strings for common UI patterns:
import { surfaceComponents } from '@/lib/theme-classes';
<div className={surfaceComponents.cardElevated}>Card</div>
<button className={surfaceComponents.buttonPrimary}>Button</button>
<div className={surfaceComponents.alertSuccess}>Saved</div>
<input className={surfaceComponents.input} />
<div className={surfaceComponents.navItem}>Nav Link</div>Full list:
cardBase,cardElevated,cardOverlaybuttonPrimary,buttonSecondary,buttonAccentalertSuccess,alertWarning,alertError,alertInfonavItem,navItemActivetextArea,codeDisplayinput,inputGroup
styleVars — CSS Variable References
For inline styles and dynamic/conditional styling:
import { styleVars } from '@/lib/theme-classes';
<div style={{ backgroundColor: styleVars.bgElevated, color: styleVars.textPrimary }}>
Dynamic styles
</div>getStatusStyle() — Dynamic Status Styles
Returns an inline style object for status indicators:
import { getStatusStyle } from '@/lib/theme-classes';
const style = getStatusStyle('success');
// { backgroundColor: 'var(--surface-success-bg)',
// color: 'var(--surface-success-text)',
// borderColor: 'var(--surface-success-border)' }
<div style={getStatusStyle('warning')}>Warning message</div>Accepts: 'info', 'success', 'warning', 'error', 'neutral'.
getButtonStyle() — Dynamic Button Styles
Returns an inline style object for buttons:
import { getButtonStyle } from '@/lib/theme-classes';
<button style={getButtonStyle('primary')}>Primary</button>
<button style={getButtonStyle('danger')}>Delete</button>Accepts: 'primary', 'secondary', 'danger', 'neutral'.
Surface Cascading
The data-surface attribute enables surface cascading. Setting data-surface="secondary" on a container remaps --surface-elevated-* and --surface-base-* CSS variables to the chosen surface for all descendant elements.
<div data-surface="secondary" class="bg-surface-secondary">
<!-- All descendants that use base/elevated surface classes
will automatically pick up secondary surface colors -->
<div class="bg-surface-elevated">
This card now uses secondary-derived colors
</div>
</div>This is used by the Layout Designer to allow sections to adopt different color schemes without changing every child component’s classes.
The cascade rules are defined in globals.css using [data-surface="X"] selectors that remap the --surface-elevated-* and --surface-base-* custom properties.
Dark Mode
Dark mode is fully automatic. The same CSS class names produce appropriate colors in both modes. CSS variables are defined twice — once in :root (light) and once in .dark (dark):
/* Light mode (default) */
:root {
--surface-base-bg: #ffffff;
--surface-base-text: #1a1a2e;
--surface-elevated-bg: #f9fafb;
/* ... */
}
/* Dark mode */
.dark {
--surface-base-bg: #0f0f1a;
--surface-base-text: #e5e5e5;
--surface-elevated-bg: #1a1a2e;
/* ... */
}No code changes are needed for dark mode support. Components that use surface classes work in both modes automatically. The user’s theme preference is stored in settings and the .dark class is toggled on the document root.
Adding New Surfaces
If you need a new surface (rare — most use cases are covered by the existing 12), follow these steps:
Step 1: Define CSS Variables
Add light and dark mode variables in src/app/globals.css:
:root {
--surface-mysurface-bg: #f0f9ff;
--surface-mysurface-text: #0c4a6e;
--surface-mysurface-text-secondary: #0369a1;
--surface-mysurface-border: #bae6fd;
--surface-mysurface-hover: #e0f2fe;
--surface-mysurface-active: #bae6fd;
--surface-mysurface-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.dark {
--surface-mysurface-bg: #0c2d48;
--surface-mysurface-text: #bae6fd;
--surface-mysurface-text-secondary: #7dd3fc;
--surface-mysurface-border: #0369a1;
--surface-mysurface-hover: #0e3a5e;
--surface-mysurface-active: #0369a1;
--surface-mysurface-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}Step 2: Add Utility Classes
In the same file, add Tailwind utility class definitions:
.bg-surface-mysurface { background-color: var(--surface-mysurface-bg); }
.text-surface-mysurface-text { color: var(--surface-mysurface-text); }
.text-surface-mysurface-text-secondary { color: var(--surface-mysurface-text-secondary); }
.border-surface-mysurface-border { border-color: var(--surface-mysurface-border); }
.hover\:bg-surface-mysurface-hover:hover { background-color: var(--surface-mysurface-hover); }
.active\:bg-surface-mysurface-active:active { background-color: var(--surface-mysurface-active); }
.shadow-surface-mysurface { box-shadow: var(--surface-mysurface-shadow); }Step 3: Add Cascade Rule
If the surface should participate in surface cascading:
[data-surface="mysurface"] {
--surface-elevated-bg: var(--surface-mysurface-bg);
--surface-elevated-text: var(--surface-mysurface-text);
--surface-elevated-border: var(--surface-mysurface-border);
/* ... etc. */
}Step 4: Add to TypeScript
Add the surface name to the SurfaceName type (if one exists) and to the surfaces object in src/lib/theme-classes.ts:
export const surfaces = {
// ... existing surfaces
mysurface: 'bg-surface-mysurface text-surface-mysurface-text border-surface-mysurface-border',
};Step 5: Add to Theme Editor
Add the new surface to the Settings theme editor dropdown so users can customize it.
Common Patterns
Buttons
// Primary action (blue)
<button className="bg-surface-primary text-surface-primary-text
hover:bg-surface-primary-hover active:bg-surface-primary-active
px-4 py-2 rounded-lg font-medium transition-colors">
Save
</button>
// Destructive action (red)
<button className="bg-surface-error text-surface-error-text
hover:bg-surface-error-hover px-4 py-2 rounded-lg">
Delete
</button>
// Secondary action
<button className="bg-surface-secondary text-surface-secondary-text
hover:bg-surface-secondary-hover px-4 py-2 rounded-lg">
Cancel
</button>
// AI/Special action (green)
<button className="bg-surface-accent text-surface-accent-text
hover:bg-surface-accent-hover px-4 py-2 rounded-lg">
Generate
</button>
// Or use the pre-composed helpers:
import { surfaceComponents } from '@/lib/theme-classes';
<button className={surfaceComponents.buttonPrimary}>Save</button>Cards
// Elevated card
<div className="bg-surface-elevated text-surface-elevated-text
border border-surface-elevated-border rounded-lg p-6
shadow-surface-elevated">
Card content
</div>
// Or:
<div className={surfaceComponents.cardElevated}>Card content</div>Status Badges
<span className="bg-surface-success text-surface-success-text
px-2 py-1 rounded text-sm">
Active
</span>
<span className="bg-surface-warning text-surface-warning-text
px-2 py-1 rounded text-sm">
Pending
</span>
<span className="bg-surface-error text-surface-error-text
px-2 py-1 rounded text-sm">
Failed
</span>
<span className="bg-surface-info text-surface-info-text
px-2 py-1 rounded text-sm">
Draft
</span>Form Inputs
<input className="bg-surface-input text-surface-input-text
border border-surface-input-border rounded-lg px-3 py-2
focus:outline-none focus:ring-2 focus:ring-surface-primary-border"
type="text"
placeholder="Enter value..."
/>
// Or:
<input className={surfaceComponents.input} type="text" />Alerts
<div className="bg-surface-warning text-surface-warning-text
border border-surface-warning-border rounded-lg p-4">
<strong>Warning:</strong> This action cannot be undone.
</div>
// Or:
<div className={surfaceComponents.alertWarning}>
<strong>Warning:</strong> This action cannot be undone.
</div>Branded Text on Neutral Background
<div className="bg-surface-base text-surface-base-text">
<h2 className="text-surface-primary-color text-xl font-bold">
Entity Name
</h2>
<p className="text-surface-base-text-secondary">
Secondary description text
</p>
</div>