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).
| Key | Default | Notes |
|---|---|---|
default | "system" | Initial color mode: "light", "dark", or "system". |
switcher | true | When 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". |
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.
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 whenconfig.theme.switcheristrue.- The
useTheme()composable (app/composables/useTheme.ts) wrapsuseColorMode()and returnscolorMode,themeOptions(Phosphor icon + i18nsettings.theme_*label perconfig.theme.options),getThemeIcon,setTheme, andcycleTheme.setThemeruns the circularstartViewTransitionanimation on desktop (skipped on mobile orprefers-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", …]).
tooling/styles/theme.css.oklch(...) values under :root for your brand (light mode). Tokens include --primary, --background, --accent, --border, --ring, --card, and --radius..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).
| Value | Component | Render | Effect |
|---|---|---|---|
staticLightRays | StaticLightRays.vue | <ClientOnly> | Fixed light-ray gradient (default - no animation cost). |
lightRays | LightRays.vue (lazy) | <ClientOnly> | Animated light rays. |
radialGradient | RadialGradient.vue | Direct (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.