Better SaaS Docs

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

  1. 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
  },
};
  1. 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 */
}
  1. 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.