Theming
Set the default color mode, the switcher, the selectable modes, and the decorative background from config.theme, and rebrand colors in the shared token sheet.
Theming is driven by config.theme (always on, no enabled flag) and rendered through next-themes, with shared shadcn tokens from @repo/styles. Modes and the switcher come from config; brand color comes from the design tokens.
config.theme
Four keys, all with defaults. default and options are typed ThemePreference ("light" | "dark" | "system"), exported from @repo/config.
| Key | Type | Default | Notes |
|---|---|---|---|
default | "light" | "dark" | "system" | "system" | Mode used before the user picks one |
switcher | boolean | true | Show the theme toggle; false hides it |
options | ThemePreference[] | ["light", "dark", "system"] | Selectable modes; the switcher cycles them in array order |
background | "lightRays" | "staticLightRays" | "radialGradient" | "staticLightRays" | Optional decorative backdrop |
Ship a single mode by setting default and reducing options (e.g. options: ["dark"]), then switcher: false to hide the toggle.
next-themes wiring
ThemeProvider (components/theme-provider.tsx) translates config into next-themes props.
// apps/web/components/theme-provider.tsx
<NextThemesProvider
attribute="class"
defaultTheme={config.theme?.default ?? "system"}
enableSystem={THEME_OPTIONS.includes("system")}
themes={[...THEME_OPTIONS].filter((option) => option !== "system")}
>enableSystemturns on only when"system"is inoptions; the remaining values become the explicitthemesset, so removing a mode in config removes it from the switcher.- next-themes injects its own anti-FOUC script and persists the choice, so the theme never flashes;
<html>carriessuppressHydrationWarning. ThemeSwitcher(components/shared/theme-switcher.tsx) is a cycle button, not a dropdown - each click advancesconfig.theme.options. The swap is instant.useTheme()fromhooks/use-theme.tsreturns{ theme, cycleTheme }for UI controls; the re-export fromcomponents/theme-provider.tsxreturns raw next-themes{ theme, setTheme, … }with no cycle logic.
Recoloring with @repo/styles
Brand color lives in design tokens, never in component code.
tooling/styles/theme.cssships the Tailwind v4 shadcn tokens (--background,--primary,--accent,--border,--ring,--radius, …) asoklch(...)values under:root.- Override the same tokens inside the
.darkblock for dark mode; thedarkcustom variant is declared inglobals.css. - Every shadcn component consumes
bg-primary,text-muted-foreground, and friends, so one token edit recolors the whole app. --primary-inkis the brand accent for TEXT on a light surface. A brand--primarybright enough for buttons is often too light for accent words, links and labels, so set--primary-inkto a darker step of the same hue that reaches 4.5:1 on--background. In the.darkblock it usually equals--primary. Fills, borders and rings keep using--primary.
Background effects
config.theme.background selects the backdrop that <ThemeBackground> renders on the marketing, onboarding, clean, and error layouts. An unset or unknown value renders nothing.
| Value | Behavior | Prerendered |
|---|---|---|
staticLightRays | Static light-ray backdrop, half strength in light mode (default) | Yes |
lightRays | Animated rays, half strength in light mode, loaded as a dynamic(..., { ssr: false }) client chunk so motion never ships unless used | No |
radialGradient | Soft radial gradient | Yes |