AI SaaS Template Docs
AI SaaS Template Docs
IntroductionQuick StartArchitecture
Vercel DeploymentDocker Deployment
ConfigurationProject StructureDatabase GuideAPI Guide
AuthenticationFile ManagementPayment & Billing
Theme Customization
Customization

Theme Customization

This guide covers theme customization, color systems, and styling patterns in AI SaaS Template.

AI SaaS Template uses a modern theme system based on Tailwind CSS v4 and shadcn/ui, supporting light/dark mode switching and fully customizable color schemes. The system uses CSS variables and the oklch color space to ensure color consistency and accessibility.

Theme Architecture

src/
├── app/
│   └── globals.css          # Global styles and CSS variable definitions
├── components/
│   ├── providers/
│   │   └── theme-provider.tsx # Next.js theme provider
│   ├── common/
│   │   └── mode-toggle.tsx    # Theme toggle component
│   └── ui/                    # shadcn/ui themed components
├── lib/
│   └── utils.ts             # Utility functions and className merging
└── components.json          # shadcn/ui configuration

Theme Configuration

shadcn/ui Configuration

The project uses the shadcn/ui component library, configured in components.json:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  },
  "iconLibrary": "lucide"
}

Tailwind CSS v4 Theme Configuration

The project uses Tailwind CSS v4's new syntax, defined in src/app/globals.css:

@theme inline {
  --font-sans: var(--font-geist-sans), system-ui, -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono",
    Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  
  /* Border radius system */
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  
  /* Color mapping */
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-secondary: var(--secondary);
  /* ... more color variables */
  
  /* Animation presets */
  --animate-accordion-down: accordion-down 0.2s ease-out;
  --animate-accordion-up: accordion-up 0.2s ease-out;
}

Theme Provider Setup

The project uses the next-themes library to implement theme switching functionality:

// src/components/providers/theme-provider.tsx
'use client'

import { ThemeProvider as NextThemesProvider } from 'next-themes'
import type * as React from 'react'

export function ThemeProvider({
  children,
  ...props
}: React.ComponentProps<typeof NextThemesProvider>) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Used in the application root layout:

// src/app/layout.tsx
import { ThemeProvider } from '@/components/providers/theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Color System

OKLCH Color Space

The project uses the OKLCH color space, which provides better perceptual consistency and gradient effects compared to traditional HSL and RGB:

/* src/app/globals.css */
:root {
  --radius: 0.625rem;
  
  /* Light theme */
  --background: oklch(1 0 0);                    /* Pure white background */
  --foreground: oklch(0.145 0 0);               /* Dark gray text */
  --card: oklch(1 0 0);                         /* Card background */
  --card-foreground: oklch(0.145 0 0);          /* Card text */
  --primary: oklch(0.205 0 0);                  /* Primary color */
  --primary-foreground: oklch(0.985 0 0);       /* Primary text */
  --secondary: oklch(0.97 0 0);                 /* Secondary color */
  --secondary-foreground: oklch(0.205 0 0);     /* Secondary text */
  --muted: oklch(0.97 0 0);                     /* Muted color */
  --muted-foreground: oklch(0.556 0 0);         /* Muted text */
  --destructive: oklch(0.577 0.245 27.325);     /* Destructive color */
  --border: oklch(0.922 0 0);                   /* Border color */
  --input: oklch(0.922 0 0);                    /* Input border */
  --ring: oklch(0.708 0 0);                     /* Focus ring */
}

.dark {
  /* Dark theme */
  --background: oklch(0.145 0 0);               /* Dark background */
  --foreground: oklch(0.985 0 0);               /* Light text */
  --card: oklch(0.205 0 0);                     /* Card background */
  --card-foreground: oklch(0.985 0 0);          /* Card text */
  --primary: oklch(0.922 0 0);                  /* Primary color */
  --primary-foreground: oklch(0.205 0 0);       /* Primary text */
  --secondary: oklch(0.269 0 0);                /* Secondary color */
  --secondary-foreground: oklch(0.985 0 0);     /* Secondary text */
  --muted: oklch(0.269 0 0);                    /* Muted color */
  --muted-foreground: oklch(0.708 0 0);         /* Muted text */
  --destructive: oklch(0.704 0.191 22.216);     /* Destructive color */
  --border: oklch(1 0 0 / 10%);                 /* Border color (with opacity) */
  --input: oklch(1 0 0 / 15%);                  /* Input (with opacity) */
  --ring: oklch(0.556 0 0);                     /* Focus ring */
}

Custom Color Variables

To add custom colors, define new CSS variables in both :root and .dark selectors:

/* src/app/globals.css */
:root {
  /* Existing variables... */
  
  /* Custom color variables */
  --success: oklch(0.641 0.15 142.495);        /* Success color */
  --success-foreground: oklch(0.09 0.016 142.495);
  --warning: oklch(0.713 0.18 85.87);          /* Warning color */
  --warning-foreground: oklch(0.09 0.018 85.87);
  --info: oklch(0.631 0.206 231.604);          /* Info color */
  --info-foreground: oklch(0.985 0 0);
  
  /* Chart colors */
  --chart-1: oklch(0.646 0.222 41.116);        /* Orange */
  --chart-2: oklch(0.6 0.118 184.704);         /* Cyan */
  --chart-3: oklch(0.398 0.07 227.392);        /* Blue */
  --chart-4: oklch(0.828 0.189 84.429);        /* Green */
  --chart-5: oklch(0.769 0.188 70.08);         /* Yellow */
}

.dark {
  /* Dark mode overrides */
  --success: oklch(0.641 0.15 142.495);
  --warning: oklch(0.713 0.18 85.87);
  --info: oklch(0.631 0.206 231.604);
  
  /* Chart color adjustments */
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
}

Using Colors in Components

// Using theme colors in components
import { cn } from '@/lib/utils'

function CustomButton({ className, variant = 'default', ...props }) {
  return (
    <button
      className={cn(
        // Base styles
        'inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors',
        // Apply different styles based on variant
        {
          'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
          'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
          'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
          'border border-input bg-background hover:bg-accent': variant === 'outline',
        },
        className
      )}
      {...props}
    />
  )
}

// Using custom colors
function StatusBadge({ status, children }) {
  return (
    <span
      className={cn(
        'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium',
        {
          'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400': status === 'success',
          'bg-yellow-50 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400': status === 'warning',
          'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400': status === 'error',
        }
      )}
    >
      {children}
    </span>
  )
}

Font System

Geist Font Configuration

The project uses Vercel's Geist font as the default font, configured in src/app/globals.css:

@theme inline {
  /* Sans-serif font */
  --font-sans: var(--font-geist-sans), system-ui, -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  
  /* Monospace font */
  --font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono",
    Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

/* Chinese font optimization */
:lang(zh) {
  font-family: var(--font-geist-sans), "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}

Font Loading Strategy

The project uses Next.js font optimization to load Geist fonts:

// src/app/layout.tsx
import { GeistSans } from 'geist/font/sans'
import { GeistMono } from 'geist/font/mono'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html 
      lang="en" 
      className={`${GeistSans.variable} ${GeistMono.variable}`}
      suppressHydrationWarning // Prevent hydration warnings during theme switching
    >
      <body className="font-sans antialiased">
        {children}
      </body>
    </html>
  )
}

Font Display Optimization

/* src/app/globals.css */
@layer base {
  /* Ensure consistent font rendering across platforms */
  html {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  body {
    @apply bg-background text-foreground font-sans;
  }
}

### Custom Font Loading

If you need to use custom fonts, place font files in the `public/fonts` directory:

```css
/* src/app/globals.css */
@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/custom-font.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Optimize font loading experience */
}

/* Use custom font in Tailwind */
.font-custom {
  font-family: 'Custom Font', var(--font-sans);
}

Use in components:

<h1 className="font-custom text-2xl font-bold">
  Using Custom Font
</h1>

Component Styling

UI Component Variants

// src/components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors",
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-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',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
  const Comp = asChild ? Slot : 'button';
  return (
    <Comp
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  );
}

Custom Component Variants

// Adding custom variants
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors",
  {
    variants: {
      variant: {
        // ... existing variants
        gradient: 'bg-gradient-to-r from-primary to-secondary text-primary-foreground',
        glass: 'bg-white/10 backdrop-blur-sm border border-white/20 text-white',
        neon: 'bg-transparent border-2 border-primary text-primary hover:bg-primary hover:text-primary-foreground shadow-[0_0_20px_rgba(59,130,246,0.5)]',
      },
      // ... rest of configuration
    },
  }
);

Dark Mode Customization

Dark Mode Styles

/* src/styles/globals.css */
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
}

Dark Mode Utilities

// src/hooks/use-theme.ts
import { useTheme } from 'next-themes';

export function useThemeUtils() {
  const { theme, setTheme, systemTheme } = useTheme();
  
  const isDark = theme === 'dark' || (theme === 'system' && systemTheme === 'dark');
  
  const toggleTheme = () => {
    setTheme(isDark ? 'light' : 'dark');
  };
  
  return {
    theme,
    isDark,
    setTheme,
    toggleTheme,
  };
}

Animation System

Animation Configuration

// src/config/theme.config.ts
export const themeConfig: ThemeConfig = {
  animations: {
    duration: {
      fast: '150ms',
      normal: '300ms',
      slow: '500ms',
    },
    easing: {
      ease: 'ease',
      easeIn: 'ease-in',
      easeOut: 'ease-out',
      easeInOut: 'ease-in-out',
    },
  },
};

Custom Animations

/* src/styles/globals.css */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideIn {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

.animate-fade-in {
  animation: fadeIn 0.3s ease-out;
}

.animate-slide-in {
  animation: slideIn 0.3s ease-out;
}

Responsive Design

Breakpoint System

// src/config/theme.config.ts
export const themeConfig: ThemeConfig = {
  breakpoints: {
    sm: '640px',
    md: '768px',
    lg: '1024px',
    xl: '1280px',
    '2xl': '1536px',
  },
};

Responsive Components

// Using responsive utilities
function ResponsiveCard() {
  return (
    <div className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4 p-4">
      <div className="bg-card rounded-lg p-6 shadow-md">
        <h3 className="text-lg md:text-xl font-semibold mb-2">
          Responsive Card
        </h3>
        <p className="text-sm md:text-base text-muted-foreground">
          This card adapts to different screen sizes.
        </p>
      </div>
    </div>
  );
}

Theme Switching

Theme Toggle Component

// src/components/theme-toggle.tsx
'use client';

import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

export function ThemeToggle() {
  const { setTheme } = useTheme();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme('light')}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('dark')}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('system')}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Creating Custom Themes

Adding New Theme Variants

If you need to add completely custom themes (like a purple theme), follow these steps:

  1. Define new theme in globals.css
/* src/app/globals.css */

/* Purple theme */
.theme-purple {
  --background: oklch(0.978 0.013 316.8);
  --foreground: oklch(0.145 0 0);
  --card: oklch(0.985 0.007 316.8);
  --card-foreground: oklch(0.145 0 0);
  --primary: oklch(0.67 0.24 310);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.956 0.013 316.8);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.956 0.013 316.8);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.956 0.013 316.8);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0.013 316.8);
  --input: oklch(0.922 0.013 316.8);
  --ring: oklch(0.67 0.24 310);
}

/* Purple theme dark mode */
.theme-purple.dark {
  --background: oklch(0.145 0.02 316.8);
  --foreground: oklch(0.985 0.007 316.8);
  --card: oklch(0.205 0.02 316.8);
  --card-foreground: oklch(0.985 0.007 316.8);
  --primary: oklch(0.922 0.02 316.8);
  --primary-foreground: oklch(0.205 0.02 316.8);
  /* ... other variables */
}
  1. Update theme provider
// src/components/providers/theme-provider.tsx
export function ThemeProvider({
  children,
  ...props
}: React.ComponentProps<typeof NextThemesProvider>) {
  return (
    <NextThemesProvider 
      attribute="class"
      defaultTheme="system"
      enableSystem
      themes={['light', 'dark', 'system', 'purple']} // Add purple theme
      {...props}
    >
      {children}
    </NextThemesProvider>
  )
}
  1. Add option in theme toggle component
// Update mode-toggle.tsx
<DropdownMenuContent align="end">
  <DropdownMenuItem onClick={() => setTheme('light')}>
    Light Mode
  </DropdownMenuItem>
  <DropdownMenuItem onClick={() => setTheme('dark')}>
    Dark Mode
  </DropdownMenuItem>
  <DropdownMenuItem onClick={() => setTheme('purple')}>
    Purple Theme
  </DropdownMenuItem>
  <DropdownMenuItem onClick={() => setTheme('system')}>
    System Settings
  </DropdownMenuItem>
</DropdownMenuContent>

Best Practices

1. Color System Usage

  • Use semantic naming: Always use semantic names like primary, secondary, muted
  • OKLCH color space: Use OKLCH for better color consistency and gradient effects
  • Contrast checking: Ensure all color combinations meet WCAG accessibility standards
  • Test both modes: Always test all UI components in both light and dark modes

2. Performance Optimization

  • CSS variables: Use CSS variables for zero-repaint theme switching
  • Font optimization: Use font-display: swap and Next.js font optimization
  • Animation control: Reduce unnecessary animations on mobile devices
  • Lazy loading: Use suppressHydrationWarning to prevent theme switching flicker

3. Developer Experience

  • TypeScript support: Use shadcn/ui's full TypeScript support
  • Component variants: Leverage class-variance-authority for type-safe component variants
  • Toolchain integration: Perfect integration with Tailwind CSS v4, Biome, Next.js
  • Hot reloading: Support real-time theme preview in development mode

4. Maintainability and Extensibility

  • Centralized configuration: All theme-related configuration centralized in globals.css and components.json
  • Modular design: Component library uses modular design for independent maintenance and testing
  • Documentation: Provide clear documentation for each custom theme and component
  • Version control: Use semantic versioning to manage theme system updates

5. Internationalization Support

  • Chinese font optimization: Optimize font sets and rendering effects for Chinese environments
  • RTL support: Use Tailwind CSS RTL modifiers to support bidirectional text
  • Number fonts: Optimize number display effects for different languages

This modern theme system provides powerful flexibility and maintainability for AI SaaS Template while ensuring excellent user and developer experience.

Payment & Billing

Previous Page

On this page

Theme ArchitectureTheme Configurationshadcn/ui ConfigurationTailwind CSS v4 Theme ConfigurationTheme Provider SetupColor SystemOKLCH Color SpaceCustom Color VariablesUsing Colors in ComponentsFont SystemGeist Font ConfigurationFont Loading StrategyFont Display OptimizationComponent StylingUI Component VariantsCustom Component VariantsDark Mode CustomizationDark Mode StylesDark Mode UtilitiesAnimation SystemAnimation ConfigurationCustom AnimationsResponsive DesignBreakpoint SystemResponsive ComponentsTheme SwitchingTheme Toggle ComponentCreating Custom ThemesAdding New Theme VariantsBest Practices1. Color System Usage2. Performance Optimization3. Developer Experience4. Maintainability and Extensibility5. Internationalization Support