Theming
SaaSForge AI uses Tailwind CSS v4 with HSL CSS custom properties. The theme is dark by default with a one-knob brand-color rebrand: change three CSS variables and the entire palette derives.
This guide covers: how the theme is wired, the one-knob rebrand, full palette swap, font changes, dark/light mode, RTL support, and verification.
How the theme is wired
| File | Role |
|---|---|
src/app/globals.css | All CSS variables for :root and .dark |
src/components/theme/ | ThemeProvider (next-themes) + theme toggle |
src/config/ui/branding.ts | Logo paths and theme defaults (default mode, toggle visibility) |
There is no tailwind.config.js: Tailwind v4 reads everything from CSS via @import "tailwindcss" and @plugin directives at the top of globals.css.
The one-knob rebrand
The shipped palette is a deep-charcoal dark theme with a blue primary. The primary color is computed from three base variables, so changing the brand is a 3-line edit:
/* src/app/globals.css :root */
--primary-hue: 221; /* 0–360. e.g., 221 = blue, 280 = purple, 25 = red */
--primary-saturation: 83%; /* 0%–100% */
--primary-lightness: 53%; /* 0%–100% */
Every --primary* variable below derives from these three:
--primary: var(--primary-hue) var(--primary-saturation) var(--primary-lightness);
--primary-hover: var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) - 5%);
--ring: var(--primary-hue) var(--primary-saturation) var(--primary-lightness);
--border-active: var(--primary-hue) var(--primary-saturation) var(--primary-lightness) / 0.5;
Save → hot reload → check buttons, links, focus rings, charts.
The accent color (used for secondary highlights) has the same three-knob pattern:
--accent-hue: 280;
--accent-saturation: 70%;
--accent-lightness: 60%;
Full variable map
Inside :root of src/app/globals.css:
/* Surfaces */
--background --foreground /* page bg + body text */
--card --card-foreground /* cards, modals */
--card-elevated /* higher-elevation surface */
--popover --popover-foreground
/* Semantic */
--primary --primary-foreground --primary-hover
--secondary --secondary-foreground
--muted --muted-foreground
--accent --accent-foreground
--destructive --destructive-foreground
/* Chrome */
--border --border-hover --border-active
--input --ring
/* Typography (8px grid + Major Third 1.25 scale) */
--body-text-size, --body-text-size-sm, --body-text-size-lg
--heading-1, --heading-2, --heading-3, --heading-4
--heading-1-weight, --heading-1-leading, --heading-1-tracking
--body-large, --body-base, --caption (with -leading variants)
--small-text-size, --small-text-size-xs
/* Spacing (8px grid: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96 px) */
--space-1 through --space-24
/* Radius */
--radius (0.75rem default), --radius-sm, --radius-lg, --radius-xl, --radius-2xl
/* Code blocks (HLJS / prose) */
--code-bg, --code-fg, --code-keyword, --code-string, ...
Switching to light-default
The template is dark by default. To flip:
- Open
src/components/theme/ThemeProvider.tsxand changedefaultTheme="dark"→"light"(or"system"). - Make sure
src/app/globals.csshas a complete:rootblock with light values for every variable. The shipped:rootblock is the dark palette: you'll need to invert it (move it into.darkand add a new:rootwith light surfaces).
A quick light palette:
:root {
--background: 0 0% 100%;
--foreground: 240 10% 10%;
--card: 0 0% 100%;
--card-foreground: 240 10% 10%;
--card-elevated: 240 5% 97%;
--secondary: 240 5% 96%;
--muted: 240 5% 96%;
--border: 0 0% 0% / 0.08;
--input: 0 0% 0% / 0.1;
/* primary/accent variables stay the same: they derive from --primary-hue */
}
Swapping fonts
The template ships with the system sans stack. To use a custom font (e.g., Inter):
- In
src/app/layout.tsx:import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); - Apply on
<html>:<html lang="en" className={inter.variable}> - Add to
globals.cssif not present::root { font-family: var(--font-sans), system-ui, sans-serif; }
For a paired serif/mono add next/font/google imports for Lora / Fira_Code and bind to --font-serif / --font-mono.
Typography scale
The scale is Major Third (1.25) on an 8px grid with fluid clamp() for headings:
--heading-1: clamp(2.4rem, 5vw, 3rem); /* 38.4px → 48px */
--heading-2: clamp(1.6rem, 4vw, 2rem); /* 25.6px → 32px */
--heading-3: 1.5rem; /* 24px */
--heading-4: 1.25rem; /* 20px */
--body-large: 1.125rem; /* 18px */
--body-base: 1rem; /* 16px */
--caption: 0.875rem; /* 14px */
Keep the ratio (or pick another from the type scale guide) when changing.
Dark / light mode
Theme switching uses next-themes with the class strategy: the provider toggles a .dark class on <html>, and the .dark { ... } block in globals.css overrides every variable.
Default theme
src/components/theme/ThemeProvider.tsx controls the default. Common values:
"dark"(current): locks new visitors to dark"light": locks to light"system": respects OS preference
Hide the toggle
In src/config/ui/branding.ts set themeConfig.showToggle: false (or whatever the equivalent flag is in your branding config).
RTL / LTR support
The template ships with direction-aware variants already wired:
@variant rtl (&:where([dir="rtl"], [dir="rtl"] *));
@variant ltr (&:where([dir="ltr"], [dir="ltr"] *));
Use them in JSX like dark mode:
<div className="ml-4 rtl:ml-0 rtl:mr-4">...</div>
Set <html dir="rtl"> (or set conditionally via a future i18n setup) to activate RTL across the app. The current app is English-only; this just keeps the door open.
Border radius
--radius: 0.75rem is the default: slightly softer than typical shadcn (0.5rem). Adjust together with --radius-sm/lg/xl/2xl for proportional changes across cards, buttons, inputs.
Verification checklist
After any theme change, click through these pages and toggle dark mode on each:
/(landing)/pricing/sign-in,/sign-up/dashboard/chat: message bubbles, model selector, send button/dashboard/documents: upload zone, document list/dashboard/billing: pricing table, plan card/dashboard/settings: form inputs, toggles
Watch for: low-contrast text on --card and --card-elevated, focus rings still visible, charts using your new palette, message-bubble background contrast against the page background.
Related guides
- Brand basics (name, logos, emails): Customization
- Component library: UI Components
- White-label launch checklist: White Label