Theming

SaaSForge Agency uses Tailwind CSS v3 with HSL CSS custom properties. Light and dark palettes ship out of the box, switching is handled by next-themes. The shipped accent is a clean blue.

This guide covers: how the theme is wired, how to change colors, swap fonts, tune dark mode, and verify across all 3 locales.

How the theme is wired

FileRole
frontend/tailwind.config.tsTailwind config: dark-mode strategy, color references to CSS vars, font family extensions
frontend/src/styles/globals.cssAll CSS variables for :root (light) and .dark (dark), plus base resets
frontend/src/app/layout.tsxRoot layout: font loading via next/font, ThemeProvider mount
frontend/src/components/shared/ThemeToggle.tsxLight/dark toggle button (uses next-themes)
frontend/src/config/brand.tsBrand identity (name, description, contact): not theme but referenced by SEO/JSON-LD

Dark mode strategy is ["class"]: a .dark class on <html> flips every variable.

The HSL color space

All colors are declared as space-separated HSL values (no hsl() wrapper around the value itself):

--primary: 221.2 83.2% 53.3%;   /* hue saturation lightness */

Tailwind utility classes wrap them at use-time: bg-primary resolves to background-color: hsl(var(--primary)).

This split lets you compose colors with alpha at the call site:

<div className="bg-primary/10">  {/* 10% opacity primary */}

Variables you'll edit most

Inside :root and .dark blocks of frontend/src/styles/globals.css:

--background          --foreground          /* page bg + body text */
--card                --card-foreground     /* cards, modals */
--popover             --popover-foreground
--primary             --primary-foreground  /* primary buttons, links */
--secondary           --secondary-foreground
--muted               --muted-foreground
--accent              --accent-foreground
--destructive         --destructive-foreground
--border              --input               --ring
--radius                                    /* global border radius (0.5rem) */
--font-serif          --font-mono

Changing the brand palette

The shipped primary is blue (221.2 83.2% 53.3%). To rebrand to (e.g.) a green:

  1. Pick HSL values. For green: H ≈ 150, S ≈ 70%, L ≈ 45%.
  2. Update :root in frontend/src/styles/globals.css:
    --primary: 150 70% 45%;
    --ring: 150 70% 45%;
    
  3. Match .dark (slightly higher L for contrast on dark surfaces):
    --primary: 150 65% 55%;
    --ring: 150 65% 55%;
    
  4. Save → hot reload → check both modes.

For a full palette swap (also --secondary, --accent, --muted), repeat with related hues. Keep L within ±10 of the originals so existing depth/contrast holds.

Generators

To get a coherent full palette without manual tuning:

Paste the generated :root and .dark blocks over the existing ones.

Logo & favicon

Theme color is just half of branding. Also swap:

  1. Logo SVGs/PNGs in frontend/public/ (replace logo.svg, logo-dark.svg, etc.).
  2. Favicon in frontend/public/favicon.ico plus the related apple-touch-icon files.
  3. Update BRAND.name and metadata in frontend/src/config/brand.ts.
  4. Runtime override: edit the site_settings singleton in Directus admin to change branding without a deploy (logo URLs, contact info, JSON-LD).

Swapping fonts

Two font slots exist (--font-serif, --font-mono); the sans is loaded via next/font in frontend/src/app/layout.tsx.

To replace the sans (e.g., to Inter):

  1. Edit frontend/src/app/layout.tsx:
    import { Inter } from "next/font/google";
    const sans = Inter({ subsets: ["latin"], variable: "--font-sans" });
    
  2. Apply on <html>:
    <html lang={locale} className={sans.variable}>
    
  3. In frontend/tailwind.config.ts ensure the fontFamily.sans extension picks up var(--font-sans):
    theme: {
      extend: {
        fontFamily: {
          sans: ["var(--font-sans)", "system-ui", "sans-serif"],
        },
      },
    },
    

For paired serif / mono changes, add next/font/google imports and bind to --font-serif / --font-mono the same way. The shipped serif is Libre Baskerville, mono is IBM Plex Mono.

Dark mode

Dark mode 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 mode

The ThemeProvider mount in frontend/src/app/layout.tsx controls the default. Common values: "dark", "light", "system".

Hide the toggle

The toggle is the <ThemeToggle /> component imported into the header (or wherever you mount it). Remove the import to hide it; the user is then locked to whatever default you set.

Make a section always-dark

To force a dark surface on a marketing section regardless of user preference:

<section className="dark bg-background text-foreground">
  {/* All children resolve dark variables */}
</section>

This is useful for hero sections that always look better dark.

Border radius

--radius: 0.5rem controls the global rounding used by rounded-lg. Lower it (0.25rem) for sharper UI; raise it (0.75rem or 1rem) for a softer brand. Components reference it indirectly via Tailwind's rounded-* utilities, so the change cascades.

RTL note

The shipped locales (EN / FR / ES) are all LTR: there is no RTL support wired in this template. The prose styles use ps- and border-s- (logical-property utilities) so they'd work under RTL if you ever set <html dir="rtl">, but the section layouts use ml-/mr- in places that would need updating.

If you add RTL: add direction-aware variants (e.g., @variant rtl ... block in globals.css), audit every section component for left/right utilities, and toggle dir based on locale.

Custom utility classes

A few helpers ship in globals.css:

  • .gradient-text: primary→accent linear gradient text fill (used on hero headlines).
  • .glass: translucent card with backdrop blur (used on overlay surfaces).
  • .prose: typography styles for blog/legal/docs WYSIWYG content from Directus.
  • Custom .container overrides for consistent max-widths.
  • Custom scrollbar styling (::-webkit-scrollbar) using --muted-foreground.

Edit these directly in frontend/src/styles/globals.css if you want different gradients, thicker scrollbars, or different .prose typography.

Verification checklist

After any theme change, click through these pages in all 3 locales and toggle dark mode on each:

  • /en, /fr, /es (homepage)
  • /en/services, /en/pricing, /en/about (and FR/ES variants)
  • /en/blog, /en/blog/[any-post-slug]
  • /en/contact: form inputs, focus rings, submit button
  • /en/legal/terms, /en/legal/privacy: .prose typography on long content

Watch for: low-contrast text on --card, focus rings still visible, footer/header chrome readable, .gradient-text gradients still show against the new palette.

Related guides