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 module
  • category: "appearance" - Groups with appearance settings
  • has_backend: false - Themes are frontend-only
  • has_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

  1. Navigate to Settings → Appearance
  2. Your theme should appear in the list
  3. Click Preview to test
  4. 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:

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

  1. Check module.json has "type": "theme"
  2. Verify theme is registered in loader-client.ts
  3. Check browser console for import errors

Colors Not Updating

  1. Ensure tokens use HSL format: hsl(H, S%, L%)
  2. Verify dark/monochrome tokens are defined
  3. Check CSS variables are being set (inspect :root)

Components Not Loading

  1. Verify component exports match expected names
  2. Check TypeScript types are compatible
  3. Ensure dynamic imports are working

Next Steps