Theme System Introduction
The SaaS Boilerplate implements a powerful build-time theme system that provides complete UI customization without modifying core code, with zero runtime overhead and maximum performance.
Overview
The theme system allows you to:
- Customize appearance - Complete control over colors, typography, spacing, and components
- Override components - Replace core components with theme-specific versions
- Add custom entities - Define theme-specific data models
- Provide translations - Multi-language support per theme
- Manage assets - Automatic asset copying and optimization
- Support dark mode - Built-in light/dark mode with system preference detection
Key Features:
- ✅ Zero runtime overhead - All themes compiled at build time
- ✅ Type-safe - Full TypeScript support with autocomplete
- ✅ Hot reload - Instant updates during development
- ✅ Registry-based - Ultra-fast theme loading (zero I/O)
- ✅ Extensible - Plugin integration and component overrides
- ✅ Accessible - WCAG 2.1 AA compliant
Architecture
Build-Time Compilation
The theme system operates at build time, not runtime:
Development:
1. Set NEXT_PUBLIC_ACTIVE_THEME environment variable
2. Run `pnpm theme:build` or `pnpm dev` (auto-builds)
3. Theme CSS compiled and assets copied
4. Registry updated with theme metadata
5. Application loads with selected theme
Production:
1. Theme compiled during build process
2. CSS bundled and optimized
3. Assets served from CDN
4. Zero runtime theme switching overhead
Performance Benefits:
- No JavaScript theme switching logic
- Optimal CSS bundling
- Perfect browser caching
- Minimal bundle size impact
Theme vs Core
The system maintains a clear separation between core and theme:
| Aspect | Core | Theme |
|---|---|---|
| Purpose | Base functionality | Visual customization |
| Location | core/ |
contents/themes/[name]/ |
| Modification | Requires code changes | Configuration only |
| Components | shadcn/ui primitives | Overrides and customs |
| Updates | Can break themes | Independent updates |
| Scope | Global system | Specific project |
Core Responsibilities:
- Authentication system
- Entity management
- API infrastructure
- Database operations
- Registry system
- Component primitives
Theme Responsibilities:
- Visual appearance (colors, fonts, spacing)
- Component styling overrides
- Brand assets (logos, images, fonts)
- Custom page templates
- Theme-specific entities
- Localized translations
Theme Selection
Environment Variable
Themes are selected via the NEXT_PUBLIC_ACTIVE_THEME environment variable:
# .env.local
NEXT_PUBLIC_ACTIVE_THEME=default
Available themes:
default- Reference implementation theme- Custom themes you create
Switching Themes
Method 1: Environment Variable
# .env.local
NEXT_PUBLIC_ACTIVE_THEME=my-theme
Then rebuild:
pnpm theme:build
Method 2: NPM Scripts
Add custom scripts to package.json:
{
"scripts": {
"theme:my-theme": "cross-env NEXT_PUBLIC_ACTIVE_THEME=my-theme pnpm theme:build",
"dev:my-theme": "cross-env NEXT_PUBLIC_ACTIVE_THEME=my-theme pnpm dev"
}
}
Usage:
pnpm theme:my-theme
pnpm dev:my-theme
Theme Directory Structure
Themes are organized in contents/themes/[theme-name]/:
contents/themes/default/
├── theme.config.ts # Theme metadata and configuration
├── app.config.ts # App-level overrides
├── dashboard.config.ts # Dashboard configuration (optional)
│
├── styles/ # CSS files (compiled at build time)
│ ├── globals.css # CSS variable overrides
│ └── components.css # Component-specific styles
│
├── public/ # Assets (auto-copied to public/theme/)
│ ├── brand/ # Logos, favicons, brand assets
│ ├── images/ # Theme images
│ ├── fonts/ # Custom fonts
│ └── docs/ # Documentation images
│
├── entities/ # Theme-specific entities
│ └── [entity]/
│ ├── [entity].config.ts
│ └── messages/
│
├── messages/ # i18n translations
│ ├── en.json
│ └── es.json
│
├── docs/ # Theme documentation
│ └── [sections]/
│
├── templates/ # Page templates (optional)
│ └── (public)/
│
└── components/ # Component overrides (optional)
├── overrides/
└── custom/
Build Process
Theme Compilation Flow
1. Read NEXT_PUBLIC_ACTIVE_THEME environment variable
↓
2. Locate theme directory: contents/themes/[theme]/
↓
3. Compile CSS from styles/ directory
↓
4. Copy assets from public/ to public/theme/
↓
5. Generate output: core/theme-styles.css
↓
6. Update theme registry with metadata
↓
7. Application imports compiled CSS
Build Scripts
Build Theme CSS:
pnpm theme:build
Build with Watch Mode:
pnpm dev # Automatically rebuilds on theme changes
Build Registry (includes theme discovery):
pnpm build:registry
Output Files
Generated CSS:
core/theme-styles.css- Compiled theme CSS (imported in app).next/theme-generated.css- Backup copy
Asset Destination:
public/theme/- All theme assets copied here
Integration with Registry System
Themes are automatically discovered and registered at build time:
// Auto-generated: core/lib/registries/theme-registry.ts
export const THEME_REGISTRY = {
default: {
name: 'default',
config: { /* ... */ },
hasStyles: true,
hasAssets: true,
hasMessages: true,
hasEntities: true,
// ... metadata
}
}
Registry Benefits:
- Zero I/O - No file system access at runtime
- Type-safe - Full TypeScript inference
- Fast lookups - Direct object access
- Rich metadata - Know exactly what each theme provides
Using the Registry
import { getTheme, getRegisteredThemes } from '@/core/lib/registries/theme-registry'
// Get active theme configuration
const theme = getTheme('default')
// Get all available themes
const themes = getRegisteredThemes()
Core vs Theme Extensibility
What Core Provides
Immutable Foundation:
- Authentication system
- Entity CRUD operations
- API infrastructure
- Database schema
- Component primitives (shadcn/ui)
- Registry system
- Build tools
Core never changes based on theme - This ensures stability and easy updates.
What Themes Customize
Visual Layer:
- Colors (CSS variables)
- Typography
- Spacing and layout
- Component styling
- Dark mode appearance
Content Layer:
- Brand assets
- Custom components
- Page templates
- Entity configurations
- Translations
Themes cannot:
- Modify core authentication logic
- Change database schema
- Override core API routes
- Break core functionality
Zero Runtime Overhead
The build-time approach eliminates runtime costs:
Traditional Runtime Theming:
// ❌ Runtime overhead
const theme = await loadTheme(themeName) // File I/O
applyTheme(theme) // DOM manipulation
recalculateStyles() // Layout recalc
Build-Time Theming:
// ✅ Zero runtime cost
import '@/core/theme-styles.css' // Already compiled
// Theme applied instantly, no JavaScript needed
Performance Comparison:
| Metric | Runtime | Build-Time |
|---|---|---|
| Initial load | ~200ms | ~5ms |
| Theme switch | ~150ms | 0ms (requires rebuild) |
| Bundle size | +15KB JS | +0KB JS |
| Layout shifts | Possible | None |
| CPU usage | High | Minimal |
Development Workflow
Creating a New Project
- Clone boilerplate
- Create custom theme (or use default)
- Configure theme (colors, fonts, assets)
- Build theme:
pnpm theme:build - Start development:
pnpm dev
Iterating on Theme
- Edit theme files (
styles/,public/,theme.config.ts) - Changes auto-rebuild (in dev mode)
- Refresh browser to see updates
- Commit theme changes to version control
Deploying to Production
- Set production theme in environment
- Build application:
pnpm build - Theme compiled during build
- Deploy with optimized assets
Use Cases
White-Label SaaS
Create multiple themes for different clients:
contents/themes/
├── client-a/ # Client A branding
├── client-b/ # Client B branding
└── client-c/ # Client C branding
Deploy with different NEXT_PUBLIC_ACTIVE_THEME per instance.
Multi-Brand Products
Maintain separate brands under one codebase:
contents/themes/
├── brand-pro/ # Professional brand
├── brand-creative/ # Creative brand
└── brand-minimal/ # Minimal brand
Seasonal Themes
Temporary visual changes:
contents/themes/
├── default/ # Year-round theme
├── holiday/ # Holiday season
└── summer/ # Summer campaign
Key Concepts
CSS Variables
All theming uses CSS custom properties:
:root {
--primary: 200 89% 47%;
--background: 0 0% 100%;
/* ... */
}
.dark {
--primary: 200 89% 60%;
--background: 240 10% 3.9%;
/* ... */
}
Components reference variables:
<Button className="bg-primary text-primary-foreground">
Click Me
</Button>
Component Overrides
Themes can replace core components:
// theme.config.ts
components: {
overrides: {
'@/core/components/ui/button': () =>
import('./components/MyButton').then(m => m.MyButton)
}
}
Asset Management
Theme assets are automatically copied:
contents/themes/my-theme/public/brand/logo.svg
↓ (build process)
public/theme/brand/logo.svg
Access in components:
<Image src="/theme/brand/logo.svg" alt="Logo" />
Getting Started
Quick Start
-
Explore default theme:
cd contents/themes/default -
Review theme.config.ts:
export const myThemeConfig: ThemeConfig = { name: 'default', displayName: 'Default Theme', // ... configuration } -
Customize colors in
styles/globals.css::root { --primary: 200 89% 47%; /* ... */ } -
Build theme:
pnpm theme:build -
Start development:
pnpm dev
Next Steps
Now that you understand the theme system architecture, dive deeper into specific topics:
- Theme Structure - Complete directory structure and file organization
- Theme Configuration - Configuring theme metadata and options
- CSS Variables and Styling - CSS variable system and styling
- Component Overrides - Customizing and extending components
- Asset Management - Managing logos, images, fonts
- Dark Mode Support - Implementing dark mode
- Theme Registry - Understanding the theme registry
- Creating Custom Themes - Step-by-step theme creation guide
💡 Tip: The default theme serves as a reference implementation. Start by customizing it rather than creating a theme from scratch.