This document explains the Custom Themes feature in SaasRock (All Editions). Tenants can customize their application's look and feel with theme presets, color pickers, CSS import/export, and AI-powered theme generation.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ THEME SYSTEM │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ THEME SETTINGS PAGE │ │
│ │ /app/:tenant/settings/theme │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Theme Presets │ │ Color Pickers │ │ Sidebar Options │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ - Default │ │ Primary │ │ - Style (default/inset)│ │ │
│ │ │ - Zinc │ │ Secondary │ │ - Force Dark Sidebar │ │ │
│ │ │ - Blue │ │ Accent │ │ │ │ │
│ │ │ - Tangerine │ │ Background │ │ [Import/Export CSS] │ │ │
│ │ │ - Sunset │ │ + 15 more... │ │ │ │ │
│ │ │ - Catppuccin │ │ │ │ │ │ │
│ │ │ - Custom │ │ (Light + Dark) │ │ │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────────┘ │ │
│ │ │ │
│ │ [Save Theme] [Reset] [Preview in Real-time] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
│
updateTenantTheme()
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ DATABASE (Prisma) │
│ │
│ Tenant { │
│ theme: String // "default" | "zinc" | "blue" | "custom" | ... │
│ themeConfig: String // JSON: { colors, darkColors, sidebar, radius, font } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
│
CSS Variables Injection
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SHADCN SIDEBAR LAYOUT │
│ │
│ :root { │
│ --primary: oklch(0.7 0.15 150); │
│ --primary-foreground: oklch(0.98 0.01 150); │
│ --secondary: oklch(0.95 0.02 150); │
│ --background: oklch(1 0 0); │
│ /* ... 19 CSS variables total */ │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
app/utils/theme/
├── ThemeConfig.ts # TypeScript types for theme configuration
├── defaultThemes.ts # Built-in theme presets
├── cssConfigParser.ts # Parse CSS string to ThemeConfig
└── cssConfigExporter.ts # Export ThemeConfig to CSS string
app/stores/
└── themePreviewStore.ts # Zustand store for real-time preview
app/components/
├── ui/colors/
│ └── ColorPicker.tsx # HSL/OKLCH color picker component
└── core/tenants/forms/
└── UpdateTenantFormTheme.tsx # Main theme settings form
app/routes/app.$tenant/settings/
├── theme.tsx # Theme settings page
└── settings.tsx # Settings layout (theme tab)
app/components/layouts/sidebars/shadcn/
└── ShadcnSidebarLayout.tsx # Injects theme CSS variables
Theme configuration is stored on the Tenant model:
model Tenant {
id String @id @default(cuid())
// ... other fields
theme String @default("default") // Preset name or "custom"
themeConfig String @default("{}") // JSON: ThemeConfig
}type ThemeConfig = {
colors: ThemeColors; // Light mode colors
darkColors: ThemeColors; // Dark mode colors
sidebar: {
style: "default" | "inset";
forceDark: boolean;
};
radius: string; // e.g., "0.5rem"
fontFamily?: string; // e.g., "Inter, sans-serif"
};
type ThemeColors = {
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
accent: string;
accentForeground: string;
background: string;
foreground: string;
card: string;
cardForeground: string;
muted: string;
mutedForeground: string;
border: string;
input: string;
ring: string;
destructive: string;
destructiveForeground: string;
popover: string;
popoverForeground: string;
};The theme system uses shadcn/ui CSS variables:
| Variable | Purpose | Example Value |
|---|---|---|
--primary |
Primary brand color | oklch(0.7 0.15 150) |
--primary-foreground |
Text on primary | oklch(0.98 0.01 150) |
--secondary |
Secondary color | oklch(0.95 0.02 150) |
--secondary-foreground |
Text on secondary | oklch(0.2 0.02 150) |
--accent |
Accent highlights | oklch(0.95 0.03 150) |
--accent-foreground |
Text on accent | oklch(0.2 0.02 150) |
--background |
Page background | oklch(1 0 0) |
--foreground |
Main text color | oklch(0.15 0.02 150) |
--card |
Card background | oklch(1 0 0) |
--card-foreground |
Card text | oklch(0.15 0.02 150) |
--muted |
Muted background | oklch(0.96 0.01 150) |
--muted-foreground |
Muted text | oklch(0.5 0.02 150) |
--border |
Border color | oklch(0.9 0.01 150) |
--input |
Input border | oklch(0.9 0.01 150) |
--ring |
Focus ring | oklch(0.7 0.15 150) |
--destructive |
Error/danger color | oklch(0.55 0.2 25) |
--destructive-foreground |
Text on destructive | oklch(0.98 0.01 25) |
--popover |
Popover background | oklch(1 0 0) |
--popover-foreground |
Popover text | oklch(0.15 0.02 150) |
Note: Colors use OKLCH format for perceptually uniform color manipulation.
Built-in presets defined in defaultThemes.ts:
| Preset | Primary Hue | Description |
|---|---|---|
default (Emerald) |
Green | Fresh, nature-inspired default |
zinc |
Gray | Neutral, professional |
blue |
Blue | Classic, trustworthy |
tangerine |
Orange | Warm, energetic |
sunset-horizon |
Sunset gradient | Warm sunset tones |
catppuccin |
Purple/Blue | Popular dev color scheme |
Additional presets available:
yellow- Warm gold tonesviolet- Creative purpleviolet-bloom- Purple gradientclaude- Anthropic stylingclaymorphism- Soft 3D effectnotebook- Paper-like appearanceocean-breeze- Calm blue-greenpastel-dreams- Soft pink/purpletwitter- X/Twitter stylevercel- Minimal black/whitesupabase- Green accentt3-chat- Pink/magentaamethyst-haze- Purple haze
The theme settings page provides:
- Preset Selector - Dropdown to choose a built-in theme
- Color Pickers - Individual pickers for all 19 colors
- Light/Dark Toggle - Edit light mode or dark mode colors
- Sidebar Options:
- Style: "default" or "inset"
- Force Dark Sidebar checkbox
- Import/Export - CSS import/export dialog with Monaco editor
- Save/Reset - Persist changes or reset to preset defaults
The themePreviewStore (Zustand) enables live preview:
import { useThemePreviewStore } from "~/stores/themePreviewStore";
// In component
const { previewConfig, setPreviewConfig } = useThemePreviewStore();
// Update preview
setPreviewConfig({
colors: { primary: newColor, ...rest },
});Preview applies CSS variables without saving to database until user clicks Save.
import { exportThemeToCSS } from "~/utils/theme/cssConfigExporter";
const css = exportThemeToCSS(themeConfig);
// Returns CSS string with :root { --primary: ...; }import { parseCSSTothemeConfig } from "~/utils/theme/cssConfigParser";
const config = parseCSSTothemeConfig(cssString);
// Returns ThemeConfig objectThis enables sharing themes via CSS snippets or importing from other sources.
The chatbot can generate and apply themes via natural language.
Located in app/modules/chatbot/lib/tools-tenant/tools-tenant-theme.server.ts:
// Example prompts:
// "Apply the emerald theme"
// "Create a cyberpunk theme with neon pink and cyan"
// "Make the primary color blue but keep everything else"- User requests theme change in chat
- AI generates ThemeConfig using OpenAI structured output
- Tool validates user has admin permission
- Theme is saved to database
onFinishcallback applies preview to UI
Only tenant admins can modify themes via chat:
const isAdmin = await getIsSuperUserOrAdminByIds({
userId: context.user.id,
tenantId: context.tenant.id,
});
if (!isAdmin) {
return { success: false, error: "Only admins can update the theme." };
}The ShadcnSidebarLayout component injects theme variables:
// In ShadcnSidebarLayout.tsx
const cssVars = useMemo(() => {
if (!tenant?.themeConfig) return {};
const config = JSON.parse(tenant.themeConfig) as ThemeConfig;
return generateCSSVariables(config);
}, [tenant?.themeConfig]);
// Applied to root element
<div style={cssVars}>
{children}
</div>Dark mode variables are applied based on user preference via .dark class.
import { getTenantWithProfile } from "~/utils/db/tenants.db.server";
const tenant = await getTenantWithProfile(tenantId);
const theme = tenant.theme; // "blue"
const config = JSON.parse(tenant.themeConfig || "{}");import { updateTenantTheme } from "~/utils/db/tenants.db.server";
await updateTenantTheme(tenantId, {
theme: "custom",
themeConfig: JSON.stringify(newConfig),
});- Edit
app/utils/theme/defaultThemes.ts - Add colors and darkColors for the new theme
- Add to the preset selector dropdown
export const defaultThemes: Record<string, ThemeConfig> = {
// Existing themes...
"my-theme": {
colors: {
primary: "oklch(0.6 0.2 280)",
// ... all 19 colors
},
darkColors: {
primary: "oklch(0.7 0.2 280)",
// ... all 19 dark colors
},
sidebar: { style: "default", forceDark: false },
radius: "0.5rem",
},
};Theme colors are automatically available via Tailwind:
// These use your theme's CSS variables
<div className="bg-primary text-primary-foreground">
Primary button
</div>
<div className="bg-secondary text-secondary-foreground">
Secondary element
</div>
<div className="text-muted-foreground">
Muted text
</div>// In a loader
export async function loader({ params }: Route.LoaderArgs) {
const tenant = await getTenantWithProfile(params.tenant);
return {
theme: tenant.theme,
themeConfig: JSON.parse(tenant.themeConfig || "{}"),
};
}Q: What color format should I use? A: OKLCH is recommended for perceptually uniform color adjustments. HSL and hex are also supported.
Q: Can I have different themes per user? A: Currently themes are per-tenant. User-specific themes would require storing preferences on the User model.
Q: How do I add custom CSS beyond colors?
A: Use the fontFamily and radius properties in ThemeConfig, or extend the CSS injection in ShadcnSidebarLayout.
Q: Does the theme apply to public pages? A: The theme applies to authenticated tenant pages. Public pages use the default theme unless explicitly configured.