GenerateSaaS

Theming

Control color mode, the theme switcher, and background effects with config.theme.

Theming is driven by config.theme in @repo/config, applied through the @nuxtjs/color-mode module and styled with the shadcn token sheet in @repo/styles. There is no enabled flag - theming is always on; the keys below set the defaults and what the switcher offers.

config.theme

All four theming options live in one block (packages/config/src/index.ts).

KeyDefaultNotes
default"system"Initial color mode: "light", "dark", or "system".
switchertrueWhen true, the theme switcher renders. When false, the control is hidden and the app stays on default.
options["light", "dark", "system"]The modes the switcher cycles through - narrow this to restrict choices.
background"staticLightRays"Ambient background effect: "lightRays", "staticLightRays", or "radialGradient".
packages/config/src/index.ts
theme: {
  default: "system",                       // light | dark | system
  switcher: true,                          // hides the switcher when false
  options: ["light", "dark", "system"],    // modes the switcher cycles
  background: "staticLightRays",           // lightRays | staticLightRays | radialGradient
},

To lock a single mode, set default and reduce options (e.g. ["dark"]); pair with switcher: false to remove the control entirely.

Color mode (Nuxt)

Light/dark switching is owned by @nuxtjs/color-mode, configured in nuxt.config.ts. It writes a bare dark class to <html> (no -mode suffix, matching Tailwind) and persists the choice in a cookie.

apps/web-nuxt/nuxt.config.ts
colorMode: {
  preference: config.theme.default,  // initial mode
  fallback: config.theme.default,    // used before "system" resolves
  storage: "cookie",                 // persisted preference
  classSuffix: "",                   // toggled class is `dark`, not `dark-mode`
},
  • ThemeSwitcher.vue (app/components/shared/) is a single cycle button (@click="cycleTheme"); it renders in the navbar and sidebar only when config.theme.switcher is true.
  • The useTheme() composable (app/composables/useTheme.ts) wraps useColorMode() and returns colorMode, themeOptions (Phosphor icon + i18n settings.theme_* label per config.theme.options), getThemeIcon, setTheme, and cycleTheme. setTheme runs the circular startViewTransition animation on desktop (skipped on mobile or prefers-reduced-motion).

Recoloring with @repo/styles

All colors are shadcn design tokens - CSS variables defined in tooling/styles/theme.css and pulled in app-wide via @repo/styles/globals.css (css: ["@repo/styles/globals.css", …]).

Open tooling/styles/theme.css.
Edit the oklch(...) values under :root for your brand (light mode). Tokens include --primary, --background, --accent, --border, --ring, --card, and --radius.
Override the same tokens inside the .dark block for dark mode.

Recolor by editing the tokens, not component classes. Components consume bg-primary, text-muted-foreground, etc., so changing one token recolors every component at once. The dark custom variant (@custom-variant dark) swaps to the .dark set automatically.

Background effects

The background key swaps the ambient visual rendered behind marketing and auth layouts (layouts/default.vue, layouts/clean.vue).

ValueComponentRenderEffect
staticLightRaysStaticLightRays.vue<ClientOnly>Fixed light-ray gradient (default - no animation cost).
lightRaysLightRays.vue (lazy)<ClientOnly>Animated light rays.
radialGradientRadialGradient.vueDirect (SSR)Soft radial gradient glow.

Both light-ray variants render inside <ClientOnly> so prerendered pages stay hydration-shift-free; radialGradient is CSS-only and renders directly during SSR. All three live in app/components/shared/background/; LightRays loads via Nuxt's auto-Lazy wrapper. The background key is optional - omit it and no ambient effect renders.

Frequently asked questions

How do I lock the app to dark mode only? Set default: "dark", options: ["dark"], and switcher: false. The switcher disappears and the app stays dark.

Where do I change the primary brand color? Edit --primary (and --primary-foreground) in tooling/styles/theme.css, under both :root and .dark.

Is the color mode remembered between visits? Yes - @nuxtjs/color-mode is configured with storage: "cookie", so the preference persists and is available during SSR.

Why does the switcher icon flash the wrong mode on first paint? On prerendered pages colorMode.preference is baked to the build-time default until hydration; ThemeSwitcher.vue guards the active value behind a mounted ref so it settles once the client takes over.

On this page