Theme Customization
This guide covers theme customization, color systems, and styling patterns in Better SaaS.
Theme System Overview
Better SaaS uses a comprehensive theme system built on top of Tailwind CSS with support for light/dark modes and custom color schemes.
Theme Architecture
src/
├── config/
│ └── theme.config.ts # Theme configuration
├── components/
│ └── providers/
│ └── theme-provider.tsx # Theme provider
├── styles/
│ └── globals.css # Global styles and CSS variables
└── components/ui/ # Themed UI components
Theme Configuration
Core Theme Config
// src/config/theme.config.ts
import type { ThemeConfig } from "@/types";
export const themeConfig: ThemeConfig = {
// Default theme
defaultTheme: 'system',
// Available themes
themes: ['light', 'dark', 'system'] as const,
// Color palette
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
// ... full color scale
900: '#1e3a8a',
950: '#172554',
},
// ... other color scales
},
// Font families
fonts: {
sans: ['var(--font-geist-sans)', 'system-ui', 'sans-serif'],
mono: ['var(--font-geist-mono)', 'ui-monospace', 'monospace'],
serif: ['ui-serif', 'Georgia', 'serif'],
},
// Border radius
borderRadius: {
none: '0',
sm: '0.375rem',
md: '0.5rem',
lg: '0.625rem',
xl: '0.75rem',
full: '9999px',
},
// Spacing scale
spacing: {
xs: '0.5rem',
sm: '0.75rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
'2xl': '3rem',
'3xl': '4rem',
},
// Animation configuration
animations: {
duration: {
fast: '150ms',
normal: '300ms',
slow: '500ms',
},
easing: {
ease: 'ease',
easeIn: 'ease-in',
easeOut: 'ease-out',
easeInOut: 'ease-in-out',
},
},
// Responsive breakpoints
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
// Box shadows
shadows: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
},
// Z-index scale
zIndex: {
dropdown: 1000,
modal: 1050,
popover: 1060,
tooltip: 1070,
toast: 1080,
},
};
Theme Provider Setup
// src/components/providers/theme-provider.tsx
'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import type { ReactNode } from 'react';
import { themeConfig } from '../../config/theme.config';
interface ThemeProviderProps {
children: ReactNode;
attribute?: 'class' | 'data-theme';
defaultTheme?: string;
enableSystem?: boolean;
disableTransitionOnChange?: boolean;
}
export function ThemeProvider({
children,
attribute = 'class',
defaultTheme = themeConfig.defaultTheme,
enableSystem = true,
disableTransitionOnChange = true,
...props
}: ThemeProviderProps) {
return (
<NextThemesProvider
attribute={attribute}
defaultTheme={defaultTheme}
enableSystem={enableSystem}
disableTransitionOnChange={disableTransitionOnChange}
{...props}
>
{children}
</NextThemesProvider>
);
}
Color System
CSS Variables
The theme system uses CSS variables for dynamic color switching:
/* src/styles/globals.css */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
/* ... more variables */
}
.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);
/* ... dark mode overrides */
}
Custom Color Scales
Add custom color scales to your theme:
// src/config/theme.config.ts
export const themeConfig: ThemeConfig = {
colors: {
// Add custom brand colors
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
// Custom semantic colors
success: {
50: '#f0fdf4',
500: '#22c55e',
900: '#14532d',
},
warning: {
50: '#fffbeb',
500: '#f59e0b',
900: '#78350f',
},
error: {
50: '#fef2f2',
500: '#ef4444',
900: '#7f1d1d',
},
},
};
Using Colors in Components
// Using theme colors in components
import { cn } from '@/lib/utils';
function CustomButton({ className, ...props }) {
return (
<button
className={cn(
'bg-brand-500 text-brand-50 hover:bg-brand-600',
'dark:bg-brand-400 dark:text-brand-900',
className
)}
{...props}
/>
);
}
Typography System
Font Configuration
// src/config/theme.config.ts
export const themeConfig: ThemeConfig = {
fonts: {
sans: [
'var(--font-geist-sans)',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'sans-serif'
],
mono: [
'var(--font-geist-mono)',
'ui-monospace',
'SFMono-Regular',
'Menlo',
'Monaco',
'Consolas',
'monospace'
],
serif: [
'ui-serif',
'Georgia',
'Cambria',
'Times New Roman',
'Times',
'serif'
],
},
};
Font Loading
// 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}`}>
<body className="font-sans">
{children}
</body>
</html>
);
}
Custom Font Weights
/* src/styles/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;
}
/* Chinese font optimization */
:lang(zh) {
font-family: var(--font-geist-sans), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}
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>
);
}
Custom Theme Creation
Creating a Custom Theme
- Define Color Palette
// src/config/custom-theme.config.ts
export const customThemeConfig = {
colors: {
primary: {
50: '#fef7ff',
100: '#fdeeff',
200: '#fcdeff',
300: '#f9c7ff',
400: '#f4a0ff',
500: '#ed70ff',
600: '#d946ef',
700: '#c026d3',
800: '#a21caf',
900: '#86198f',
950: '#581c87',
},
// ... other colors
},
};
- Update CSS Variables
/* src/styles/custom-theme.css */
.theme-purple {
--primary: oklch(0.67 0.24 310);
--primary-foreground: oklch(0.985 0 0);
/* ... other variables */
}
- Apply Custom Theme
// src/components/theme-provider.tsx
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
themes={['light', 'dark', 'purple']}
>
{children}
</NextThemesProvider>
);
}
Best Practices
1. Consistent Color Usage
- Use semantic color names (primary, secondary, accent)
- Maintain consistent contrast ratios
- Test colors in both light and dark modes
2. Performance Optimization
- Use CSS variables for dynamic theming
- Minimize animation usage on mobile
- Optimize font loading with font-display: swap
3. Accessibility
- Ensure sufficient color contrast
- Test with screen readers
- Provide theme preferences
4. Maintainability
- Keep theme configuration centralized
- Use TypeScript for type safety
- Document custom color scales
This comprehensive theme system provides flexibility while maintaining consistency across your Better SaaS application.