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

FileRole
src/app/globals.cssAll CSS variables for :root and .dark
src/components/theme/ThemeProvider (next-themes) + theme toggle
src/config/ui/branding.tsLogo 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:

  1. Open src/components/theme/ThemeProvider.tsx and change defaultTheme="dark""light" (or "system").
  2. Make sure src/app/globals.css has a complete :root block with light values for every variable. The shipped :root block is the dark palette: you'll need to invert it (move it into .dark and add a new :root with 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):

  1. In src/app/layout.tsx:
    import { Inter } from "next/font/google";
    const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
    
  2. Apply on <html>:
    <html lang="en" className={inter.variable}>
    
  3. Add to globals.css if 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