Developer ReferenceTheme Customization

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.

SurfacePurposeLight DefaultDark Default
baseMain page background, content areasWhite bg, dark textDark bg, light text
elevatedCards, panels, sidebars, sectionsLight gray bg, dark textSlightly lighter bg, light text
overlayModals, dropdowns, popovers, tooltipsWhite bg with shadow, dark textDark 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.

SurfacePurposeLight DefaultDark Default
primaryMain brand actions, CTAs, active statesBlue bg, white textBlue bg, white text
secondarySecondary actions, alternative choicesPurple bg, white textPurple bg, white text
accentHighlights, badges, special emphasis, AI featuresGreen bg, white textGreen 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.

SurfacePurposeLight DefaultDark Default
successSuccess confirmations, positive statesGreen-tinted bg, dark green textDark green bg, light green text
warningCaution alerts, pending statesYellow-tinted bg, dark yellow textDark yellow bg, light yellow text
errorError alerts, destructive states, delete actionsRed-tinted bg, dark red textDark red bg, light red text
infoInformational notices, tips, help textBlue-tinted bg, dark blue textDark blue bg, light blue text

Specialized Surfaces

SurfacePurposeDescription
navigationSidebar, top nav, nav itemsCoordinates with the layout surfaces but independently configurable
inputForm inputs, text fields, selects, textareasProvides 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
PropertyVariable SuffixPurpose
Background-bgMain background color
Text-textPrimary text color on this surface
Text Secondary-text-secondaryMuted/secondary text on this surface
Border-borderBorder color for elements on this surface
Hover-hoverBackground on hover state
Active-activeBackground on active/pressed state
Shadow-shadowBox 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 background

Text

text-surface-{name}-text                Primary text on this surface
text-surface-{name}-text-secondary      Secondary/muted text on this surface

Border

border-surface-{name}-border            Border color for this surface

Shadow

shadow-surface-{name}                   Box shadow for this surface

Brand 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 text

These 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 border

Surface 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-change messages 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, cardOverlay
  • buttonPrimary, buttonSecondary, buttonAccent
  • alertSuccess, alertWarning, alertError, alertInfo
  • navItem, navItemActive
  • textArea, codeDisplay
  • input, 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>