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:
- 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 */
}
- 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>
)
}
- 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
andcomponents.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.