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
| File | Role |
|---|---|
frontend/tailwind.config.ts | Tailwind config: dark-mode strategy, color references to CSS vars, font family extensions |
frontend/src/styles/globals.css | All CSS variables for :root (light) and .dark (dark), plus base resets |
frontend/src/app/layout.tsx | Root layout: font loading via next/font, ThemeProvider mount |
frontend/src/components/shared/ThemeToggle.tsx | Light/dark toggle button (uses next-themes) |
frontend/src/config/brand.ts | Brand 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:
- Pick HSL values. For green: H ≈ 150, S ≈ 70%, L ≈ 45%.
- Update
:rootinfrontend/src/styles/globals.css:--primary: 150 70% 45%; --ring: 150 70% 45%; - Match
.dark(slightly higher L for contrast on dark surfaces):--primary: 150 65% 55%; --ring: 150 65% 55%; - 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:
- tweakcn.com: visual editor for shadcn/ui themes; export the CSS-vars block.
- shadcn theme generator: official, HSL output.
Paste the generated :root and .dark blocks over the existing ones.
Logo & favicon
Theme color is just half of branding. Also swap:
- Logo SVGs/PNGs in
frontend/public/(replacelogo.svg,logo-dark.svg, etc.). - Favicon in
frontend/public/favicon.icoplus the relatedapple-touch-iconfiles. - Update
BRAND.nameand metadata infrontend/src/config/brand.ts. - Runtime override: edit the
site_settingssingleton 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):
- Edit
frontend/src/app/layout.tsx:import { Inter } from "next/font/google"; const sans = Inter({ subsets: ["latin"], variable: "--font-sans" }); - Apply on
<html>:<html lang={locale} className={sans.variable}> - In
frontend/tailwind.config.tsensure thefontFamily.sansextension picks upvar(--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
.containeroverrides 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:.prosetypography 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
- UI Components: the component catalog
- UI Customization: editing components, adding sections
- Content Editing: editing copy via Directus