Color Themes & Dark Mode

Nine pre-built color themes with dark mode support — switch with a single environment variable

Kit ships with 9 pre-built color themes that change the entire look of your application through a single COLOR_THEME environment variable. Each theme provides both light and dark mode variants, powered by next-themes for system preference detection and flicker-free theme switching.
This page covers theme selection, dark mode, CSS custom properties, and creating your own themes. For component styling, see UI Components. For Tailwind configuration, see Customization.

How It Works

The theme system flows from an environment variable through CSS custom properties to Tailwind utility classes:
COLOR_THEME env variable
    |
    v
getActiveTheme() — validates env var, falls back to 'default'
    |
    v
data-theme="ocean" on <html> — set in root layout at build time
    |
    v
CSS selectors [data-theme='ocean'] — activate the matching theme CSS
    |
    v
CSS Custom Properties (--primary, --background, ...)
    |
    v
Tailwind classes (bg-primary, text-foreground, ...)
    |
    v
Dark mode toggle — adds .dark class → [data-theme='ocean'].dark selectors
The color theme (which palette) and the dark mode (light vs dark variant) are independent systems. Switching from ocean to forest changes the color palette. Toggling dark mode switches between the light and dark variants of whichever theme is active.

Available Themes

The theme registry defines all 9 available themes:
src/styles/themes/themes.ts — Available Themes
export const AVAILABLE_THEMES = [
  'default',
  'ocean',
  'forest',
  'sunset',
  'midnight',
  'coral',
  'slate',
  'aurora',
  'crimson',
] as const
Each theme is designed for a specific use case:
ThemeColorBest For
defaultBlueProfessional SaaS, general-purpose applications
oceanTealMaritime, tech startups, developer tools
forestGreenEco-friendly, sustainability, health & wellness
sunsetOrangeCreative tools, design platforms, marketplaces
midnightPurplePremium enterprise, fintech, luxury products
coralPinkConsumer apps, social platforms, lifestyle brands
slateGrayBusiness tools, B2B platforms, analytics dashboards
auroraCyanTech platforms, AI products, cutting-edge startups
crimsonRedPerformance tools, analytics, gaming platforms

Switching Themes

To change your application's theme, set the COLOR_THEME environment variable in your apps/boilerplate/.env.local:
bash
# apps/boilerplate/.env.local
COLOR_THEME=ocean
The getActiveTheme() function reads this variable and validates it against the list of available themes. If the value is missing or invalid, it falls back to default:
src/styles/themes/themes.ts — getActiveTheme()
export function getActiveTheme(): ThemeName {
  const envTheme =
    process.env.COLOR_THEME || process.env.NEXT_PUBLIC_COLOR_THEME

  if (envTheme && AVAILABLE_THEMES.includes(envTheme as ThemeName)) {
    return envTheme as ThemeName
  }

  // Fallback to default theme
  return 'default'
}
The root layout calls this function and applies the result as a data-theme attribute on the <html> element:
src/app/layout.tsx — Theme Application
// Get active color theme from environment variable
  const colorTheme = getActiveTheme()

Dark Mode

Dark mode is handled separately from the color theme. It uses next-themes to toggle a .dark class on the <html> element, which activates the dark variant of the current color theme.

ThemeProvider

The ThemeProvider wraps the application and manages dark mode state. It detects system preference, persists user choice to localStorage, and applies the .dark class:
src/providers/theme-provider.tsx
'use client'

import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'

export function ThemeProvider({
  children,
  ...props
}: React.ComponentProps<typeof NextThemesProvider>) {
  return (
    <NextThemesProvider
      attribute="class"
      defaultTheme="system"
      enableSystem={true}
      disableTransitionOnChange
      {...props}
    >
      {children}
    </NextThemesProvider>
  )
}
The provider is configured in the root layout inside the provider hierarchy:
src/app/layout.tsx — Provider Hierarchy with ThemeProvider
const content = (
    <html lang="en" suppressHydrationWarning data-theme={colorTheme}>
      <body className={`${inter.className} ${jetbrainsMono.variable}`}>
        <MSWProvider>
          <QueryProvider>
            <ThemeProvider
              attribute="class"
              defaultTheme="system"
              enableSystem
              disableTransitionOnChange
            >
              <DemoProvider>
                  {children}
                  <Toaster richColors position="top-center" duration={8000} />
                  {process.env.VERCEL_ENV === 'production' && <Analytics />}
                  {process.env.VERCEL_ENV === 'production' && <SpeedInsights />}
              </DemoProvider>
            </ThemeProvider>
          </QueryProvider>
        </MSWProvider>
      </body>
    </html>
  )

ThemeToggle

The ThemeToggle component provides a sun/moon button for users to switch between light and dark modes:
packages/ui/src/theme-toggle.tsx — Dark Mode Toggle
'use client'

import * as React from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'

import { Button } from './button'

export function ThemeToggle() {
  // HYDRATION-FIX: Use resolvedTheme instead of theme
  // `theme` can be 'system' which doesn't tell us the actual theme
  // `resolvedTheme` always returns 'light' or 'dark' after hydration
  const { resolvedTheme, setTheme } = useTheme()
  const [mounted, setMounted] = React.useState(false)

  // useEffect only runs on the client, so now we can safely show the UI
  React.useEffect(() => {
    setMounted(true)
  }, [])

  const toggleTheme = () => {
    setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
  }

  if (!mounted) {
    return (
      <Button variant="ghost" size="icon" disabled className="border-0 bg-transparent text-foreground/70 hover:bg-transparent hover:text-foreground">
        <Moon className="h-[1.2rem] w-[1.2rem]" />
        <span className="sr-only">Toggle theme</span>
      </Button>
    )
  }

  return (
    <Button variant="ghost" size="icon" onClick={toggleTheme} className="border-0 bg-transparent text-foreground/70 hover:bg-transparent hover:border-0 hover:text-foreground">
      {resolvedTheme === 'dark' ? (
        <Moon className="transition-all" />
      ) : (
        <Sun className="transition-all" />
      )}
      <span className="sr-only">Toggle theme</span>
    </Button>
  )
}
Key implementation details:
  • resolvedTheme is used instead of theme because theme can return 'system' which doesn't tell you the actual resolved mode
  • mounted state prevents hydration mismatch — the toggle renders a disabled placeholder during SSR and only shows the interactive version after client-side hydration
  • The toggle calls setTheme() from next-themes, which adds or removes the .dark class on <html>

CSS Custom Properties

Each theme defines its colors as CSS custom properties using HSL values without the hsl() wrapper. This is the foundation that makes the entire color system work with Tailwind's opacity modifiers.
Here is the default theme's light mode variables:
src/styles/themes/default.css — Light Mode Variables
[data-theme='default'] {
  /* Light Mode */
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  --card: 0 0% 100%;
  --card-foreground: 0 0% 3.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 0 0% 3.9%;
  --primary: 221 83% 53%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 210 40% 98%;
  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 221 83% 53%;
  --gradient-start: 221 83% 53%;
  --gradient-end: 199 89% 48%;
  --radius: 0.5rem;
  --chart-1: 12 76% 61%;
  --chart-2: 173 58% 39%;
  --chart-3: 197 37% 24%;
  --chart-4: 43 74% 66%;
  --chart-5: 27 87% 67%;
  /* Status Colors */
  --success: 142 76% 36%;
  --success-foreground: 0 0% 100%;
  --warning: 32 95% 44%;
  --warning-foreground: 0 0% 100%;
  --info: 199 89% 48%;
  --info-foreground: 0 0% 100%;
  /* Footer Colors */
  --footer-background: 222 47% 11%;
  --footer-foreground: 210 40% 98%;
}
And the dark mode variant:
src/styles/themes/default.css — Dark Mode Variables
[data-theme='default'].dark {
  /* Dark Mode */
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  --card: 222.2 84% 4.9%;
  --card-foreground: 210 40% 98%;
  --popover: 222.2 84% 4.9%;
  --popover-foreground: 210 40% 98%;
  --primary: 217.2 91.2% 59.8%;
  --primary-foreground: 222.2 47.4% 11.2%;
  --secondary: 217.2 32.6% 17.5%;
  --secondary-foreground: 210 40% 98%;
  --muted: 217.2 32.6% 17.5%;
  --muted-foreground: 215 20.2% 65.1%;
  --accent: 217.2 32.6% 17.5%;
  --accent-foreground: 210 40% 98%;
  --destructive: 0 62.8% 30.6%;
  --destructive-foreground: 210 40% 98%;
  --border: 217.2 32.6% 17.5%;
  --input: 217.2 32.6% 17.5%;
  --ring: 224.3 76.3% 48%;
  --gradient-start: 217 91% 60%;
  --gradient-end: 199 89% 48%;
  --chart-1: 220 70% 50%;
  --chart-2: 160 60% 45%;
  --chart-3: 30 80% 55%;
  --chart-4: 280 65% 60%;
  --chart-5: 340 75% 55%;
  /* Status Colors */
  --success: 142 76% 50%;
  --success-foreground: 0 0% 100%;
  --warning: 32 95% 54%;
  --warning-foreground: 0 0% 100%;
  --info: 199 89% 58%;
  --info-foreground: 0 0% 100%;
  /* Footer Colors */
  --footer-background: 222 84% 3%;
  --footer-foreground: 210 40% 98%;
}

Variable Reference

VariablePurposeExample Usage
--backgroundPage backgroundbg-background
--foregroundPrimary text colortext-foreground
--cardCard/surface backgroundbg-card
--card-foregroundText on cardstext-card-foreground
--primaryBrand/accent colorbg-primary, text-primary
--primary-foregroundText on primary backgroundstext-primary-foreground
--secondarySecondary elementsbg-secondary
--mutedSubtle backgroundsbg-muted
--muted-foregroundSubdued texttext-muted-foreground
--accentHover/focus highlightsbg-accent
--destructiveError/danger statesbg-destructive
--borderBorders and dividersborder-border
--inputForm input bordersborder-input
--ringFocus ring colorring-ring
--gradient-start / --gradient-endBrand gradientbg-gradient-brand
--chart-1 through --chart-5Chart/data visualizationChart components
--radiusBase border radiusrounded-lg, rounded-md

Creating a Custom Theme

1

Create a CSS file

Add a new file in apps/boilerplate/src/styles/themes/ with your theme's color variables. Use the default theme as a starting point:
css
/* apps/boilerplate/src/styles/themes/ruby.css */
[data-theme='ruby'] {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  --primary: 346 77% 49%;
  --primary-foreground: 0 0% 98%;
  /* ... define all variables */
}

[data-theme='ruby'].dark {
  --background: 346 30% 6%;
  --foreground: 0 0% 98%;
  --primary: 346 77% 59%;
  --primary-foreground: 0 0% 9%;
  /* ... define all dark mode variables */
}
2

Register the theme

Add your theme name to the AVAILABLE_THEMES array and THEME_METADATA object in apps/boilerplate/src/styles/themes/themes.ts:
typescript
export const AVAILABLE_THEMES = [
  'default',
  'ocean',
  // ... existing themes
  'ruby',  // Add your new theme
] as const
3

Import in globals.css

Add the import at the top of apps/boilerplate/src/app/globals.css, alongside the other theme imports:
css
@import '../styles/themes/ruby.css';
4

Set the environment variable

Update your apps/boilerplate/.env.local to use the new theme:
bash
COLOR_THEME=ruby
Restart your dev server and the new theme is active.

Key Files

FilePurpose
apps/boilerplate/src/styles/themes/themes.tsTheme registry — available themes, metadata, getActiveTheme()
apps/boilerplate/src/styles/themes/default.cssDefault blue theme CSS variables (light + dark)
apps/boilerplate/src/styles/themes/*.cssAll 9 theme CSS files (ocean, forest, sunset, etc.)
apps/boilerplate/src/app/layout.tsxRoot layout — applies data-theme attribute to <html>
apps/boilerplate/src/providers/theme-provider.tsxThemeProvider wrapper for next-themes
packages/ui/src/theme-toggle.tsxDark mode toggle button component
apps/boilerplate/src/app/globals.cssGlobal styles — imports all theme CSS files