Creating Themes
This guide walks you through creating a custom theme for Auto Commerce. You'll learn the module structure, token definitions, component implementations, and registration process.
Prerequisites
- Node.js 18+
- Familiarity with React and TypeScript
- Understanding of Tailwind CSS
- Basic knowledge of the Auto Commerce module system
Theme Module Structure
Themes are standard Auto Commerce modules with a specific structure:
modules/ThemeCustom/
├── module.json # Module metadata
└── frontend/
├── index.tsx # Theme configuration & exports
├── tokens.ts # Design tokens (optional, can be inline)
└── components/
├── ui/ # UI primitive overrides
│ ├── button.tsx
│ ├── card.tsx
│ └── ...
├── layouts/ # Layout components
│ └── EnterpriseLayout.tsx
└── templates/ # Page templates
├── LoginTemplate.tsx
├── DashboardTemplate.tsx
└── ...
Step 1: Create Module Structure
module.json
{
"name": "ThemeCustom",
"display_name": "Custom Theme",
"description": "A custom theme with unique styling",
"version": "1.0.0",
"type": "theme",
"category": "appearance",
"author": "Your Name",
"enabled_by_default": false,
"visibility": "global",
"settings_route": null,
"has_backend": false,
"has_frontend": true,
"dependencies": [],
"permissions": [],
"license_required": false,
"icon": "Palette"
}
Key fields:
type: "theme"- Identifies this as a theme modulecategory: "appearance"- Groups with appearance settingshas_backend: false- Themes are frontend-onlyhas_frontend: true- Required for themes
Step 2: Define Design Tokens
Create your color palette and design system:
// modules/ThemeCustom/frontend/tokens.ts
import type { DesignTokens } from '@/lib/theme/types';
export const customTokens: DesignTokens = {
colors: {
// Primary - Your brand color
primary: 'hsl(200, 80%, 50%)', // Cyan
primaryForeground: 'hsl(0, 0%, 100%)',
// Secondary
secondary: 'hsl(200, 20%, 95%)',
secondaryForeground: 'hsl(200, 80%, 30%)',
// Accent
accent: 'hsl(180, 60%, 95%)',
accentForeground: 'hsl(180, 60%, 30%)',
// Muted
muted: 'hsl(200, 10%, 96%)',
mutedForeground: 'hsl(200, 10%, 45%)',
// Destructive
destructive: 'hsl(0, 70%, 55%)',
destructiveForeground: 'hsl(0, 0%, 100%)',
// Background
background: 'hsl(0, 0%, 100%)',
foreground: 'hsl(200, 50%, 10%)',
// Card
card: 'hsl(0, 0%, 100%)',
cardForeground: 'hsl(200, 50%, 10%)',
// Border & Input
border: 'hsl(200, 20%, 90%)',
input: 'hsl(200, 20%, 90%)',
ring: 'hsl(200, 80%, 50%)',
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
'2xl': '3rem',
},
borderRadius: {
sm: '0.375rem',
md: '0.5rem',
lg: '0.75rem',
full: '9999px',
},
fontFamily: {
sans: 'Inter, system-ui, sans-serif',
mono: 'JetBrains Mono, monospace',
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
},
shadows: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
},
};
// Dark mode tokens
export const customDarkTokens: DesignTokens = {
...customTokens,
colors: {
primary: 'hsl(200, 80%, 60%)',
primaryForeground: 'hsl(200, 80%, 10%)',
secondary: 'hsl(200, 30%, 20%)',
secondaryForeground: 'hsl(200, 20%, 90%)',
accent: 'hsl(180, 50%, 20%)',
accentForeground: 'hsl(180, 40%, 90%)',
muted: 'hsl(200, 20%, 18%)',
mutedForeground: 'hsl(200, 10%, 60%)',
destructive: 'hsl(0, 60%, 45%)',
destructiveForeground: 'hsl(0, 0%, 100%)',
background: 'hsl(200, 30%, 8%)',
foreground: 'hsl(0, 0%, 95%)',
card: 'hsl(200, 30%, 12%)',
cardForeground: 'hsl(0, 0%, 95%)',
border: 'hsl(200, 20%, 20%)',
input: 'hsl(200, 20%, 20%)',
ring: 'hsl(200, 80%, 60%)',
},
};
// Monochrome tokens
export const customMonochromeTokens: DesignTokens = {
...customTokens,
colors: {
primary: 'hsl(0, 0%, 20%)',
primaryForeground: 'hsl(0, 0%, 100%)',
secondary: 'hsl(0, 0%, 95%)',
secondaryForeground: 'hsl(0, 0%, 20%)',
accent: 'hsl(0, 0%, 92%)',
accentForeground: 'hsl(0, 0%, 20%)',
muted: 'hsl(0, 0%, 96%)',
mutedForeground: 'hsl(0, 0%, 45%)',
destructive: 'hsl(0, 0%, 30%)',
destructiveForeground: 'hsl(0, 0%, 100%)',
background: 'hsl(0, 0%, 100%)',
foreground: 'hsl(0, 0%, 9%)',
card: 'hsl(0, 0%, 100%)',
cardForeground: 'hsl(0, 0%, 9%)',
border: 'hsl(0, 0%, 90%)',
input: 'hsl(0, 0%, 90%)',
ring: 'hsl(0, 0%, 20%)',
},
};
Step 3: Create Custom Components (Optional)
Override components that need unique styling:
Custom Button
// modules/ThemeCustom/frontend/components/ui/button.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
return (
<button
className={cn(
// Base styles
'inline-flex items-center justify-center rounded-lg font-medium',
'transition-all duration-200 ease-out',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:pointer-events-none disabled:opacity-50',
// Variant styles
{
'default': 'bg-primary text-primary-foreground shadow-md hover:shadow-lg hover:scale-[1.02]',
'destructive': 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
'outline': 'border-2 border-primary text-primary hover:bg-primary hover:text-primary-foreground',
'secondary': 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
'ghost': 'hover:bg-accent hover:text-accent-foreground',
'link': 'text-primary underline-offset-4 hover:underline',
}[variant],
// Size styles
{
'default': 'h-10 px-5 py-2',
'sm': 'h-8 px-3 text-sm',
'lg': 'h-12 px-8 text-lg',
'icon': 'h-10 w-10',
}[size],
className
)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button };
Custom Card
// modules/ThemeCustom/frontend/components/ui/card.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border border-border bg-card text-card-foreground',
'shadow-sm hover:shadow-md transition-shadow duration-200',
className
)}
{...props}
/>
)
);
Card.displayName = 'Card';
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
)
);
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('text-xl font-semibold leading-none tracking-tight', className)} {...props} />
)
);
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
)
);
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
);
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
)
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
Step 4: Create Templates
Templates define how entire pages look:
// modules/ThemeCustom/frontend/components/templates/LoginTemplate.tsx
'use client';
import React from 'react';
import type { LoginTemplateProps } from '@/lib/theme/interfaces/templates';
import { Button } from '../ui/button';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card';
export function LoginTemplate({
onSubmit,
loading,
error,
tenantName,
}: LoginTemplateProps) {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 to-accent/10">
<Card className="w-full max-w-md mx-4">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Welcome back</CardTitle>
<CardDescription>
Sign in to {tenantName || 'your account'}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-3 rounded-lg bg-destructive/10 text-destructive text-sm">
{error}
</div>
)}
<div className="space-y-2">
<label className="text-sm font-medium">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full h-10 px-3 rounded-lg border border-input bg-background"
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full h-10 px-3 rounded-lg border border-input bg-background"
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Signing in...' : 'Sign in'}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}
Step 5: Create Theme Configuration
Assemble everything in the main index file:
// modules/ThemeCustom/frontend/index.tsx
import type { ThemeConfig, ThemeComponents } from '@/lib/theme/types';
import { customTokens, customDarkTokens, customMonochromeTokens } from './tokens';
// Custom components
import { Button } from './components/ui/button';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './components/ui/card';
// Templates
import { LoginTemplate } from './components/templates/LoginTemplate';
import { DashboardTemplate } from './components/templates/DashboardTemplate';
// For components we don't override, import from shadcn
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
// ... import other shadcn components as needed
const components: Partial<ThemeComponents> = {
// Custom components
Button: Button as any,
Card: Card as any,
CardHeader: CardHeader as any,
CardTitle: CardTitle as any,
CardDescription: CardDescription as any,
CardContent: CardContent as any,
CardFooter: CardFooter as any,
// Re-exported shadcn components
Input: Input as any,
Label: Label as any,
Checkbox: Checkbox as any,
// Templates
LoginTemplate: LoginTemplate as any,
DashboardTemplate: DashboardTemplate as any,
};
export const themeConfig: ThemeConfig = {
definition: {
id: 'theme-custom',
name: 'ThemeCustom',
displayName: 'Custom Theme',
description: 'A custom theme with unique styling',
version: '1.0.0',
framework: 'tailwind',
previewImage: '/themes/custom-preview.png',
author: 'Your Name',
tags: ['custom', 'modern'],
},
components,
tokens: customTokens,
darkTokens: customDarkTokens,
monochromeTokens: customMonochromeTokens,
moduleAlias: 'theme-custom',
};
export default themeConfig;
Step 6: Register the Theme
Add your theme to the registry:
// frontend/lib/theme/loader-client.ts
export function registerBuiltInThemes(): void {
// ... existing themes
// Register Custom theme
const customRegistration: ThemeRegistration = {
definition: {
id: 'theme-custom',
name: 'ThemeCustom',
displayName: 'Custom Theme',
description: 'A custom theme with unique styling',
version: '1.0.0',
framework: 'tailwind',
previewImage: '/themes/custom-preview.png',
author: 'Your Name',
tags: ['custom', 'modern'],
},
loader: async () => {
const module = await import('@modules/ThemeCustom/frontend/index');
return module.themeConfig || module.default;
},
isDefault: false,
};
registerTheme(customRegistration);
}
Step 7: Add Preview Image
Create a preview image at public/themes/custom-preview.png (recommended size: 400x300).
Testing Your Theme
Run Type Check
cd frontend && npm run typecheck
Start Development Server
cd frontend && npm run dev
Test in Browser
- Navigate to Settings → Appearance
- Your theme should appear in the list
- Click Preview to test
- Click Apply to activate
Best Practices
1. Use CSS Variables
Always use Tailwind's CSS variable classes:
// Good
className="bg-background text-foreground border-border"
// Bad
className="bg-white text-gray-900 border-gray-200"
2. Test All Color Modes
Verify your theme works in:
- Light mode
- Dark mode
- Monochrome mode
3. Maintain Contrast
Use contrast checking tools to ensure accessibility:
- WebAIM Contrast Checker
- Aim for 4.5:1 minimum for text
4. Keep Components Consistent
If you override one component in a group, consider overriding related ones:
- Card → CardHeader, CardTitle, CardContent, CardFooter
- Dialog → DialogHeader, DialogTitle, DialogContent, etc.
5. Document Customizations
Comment any significant styling decisions:
// Using larger border radius for softer appearance
borderRadius: {
sm: '0.5rem', // 8px instead of default 4px
md: '0.75rem',
lg: '1rem',
full: '9999px',
},
Troubleshooting
Theme Not Appearing
- Check
module.jsonhas"type": "theme" - Verify theme is registered in
loader-client.ts - Check browser console for import errors
Colors Not Updating
- Ensure tokens use HSL format:
hsl(H, S%, L%) - Verify dark/monochrome tokens are defined
- Check CSS variables are being set (inspect
:root)
Components Not Loading
- Verify component exports match expected names
- Check TypeScript types are compatible
- Ensure dynamic imports are working
Next Steps
- Design Tokens - Deep dive into token system
- Components - Component abstraction details
- Color Modes - Understanding color mode system