Farbthemen & Dark Mode

Neun vorgefertigte Farbthemen mit Dark-Mode-Unterstützung — wechsle mit einer einzigen Umgebungsvariable

Das Kit wird mit 9 vorgefertigten Farbthemen geliefert, die das gesamte Erscheinungsbild deiner Anwendung über eine einzige COLOR_THEME-Umgebungsvariable verändern. Jedes Theme bietet sowohl einen Light- als auch einen Dark-Mode-Variant, powered by next-themes für die Erkennung der Systemeinstellung und ein flimmerfreies Theme-Wechseln.
Diese Seite behandelt die Theme-Auswahl, den Dark Mode, CSS Custom Properties und das Erstellen eigener Themes. Für das Styling von Komponenten, siehe UI-Komponenten. Für die Tailwind-Konfiguration, siehe Anpassung.

So funktioniert es

Das Theme-System fließt von einer Umgebungsvariable über CSS Custom Properties zu Tailwind-Utility-Klassen:
COLOR_THEME Umgebungsvariable
    |
    v
getActiveTheme() — validiert die Umgebungsvariable, fällt auf 'default' zurück
    |
    v
data-theme="ocean" auf <html> — wird im Root-Layout zur Build-Zeit gesetzt
    |
    v
CSS-Selektoren [data-theme='ocean'] — aktivieren das passende Theme-CSS
    |
    v
CSS Custom Properties (--primary, --background, ...)
    |
    v
Tailwind-Klassen (bg-primary, text-foreground, ...)
    |
    v
Dark-Mode-Toggle — fügt .dark-Klasse hinzu → [data-theme='ocean'].dark-Selektoren
Das Farbthema (welche Palette) und der Dark Mode (helle vs. dunkle Variante) sind unabhängige Systeme. Der Wechsel von ocean zu forest ändert die Farbpalette. Das Umschalten des Dark Modes wechselt zwischen der hellen und der dunklen Variante des jeweils aktiven Themes.

Verfügbare Themes

Die Theme-Registry definiert alle 9 verfügbaren Themes:
src/styles/themes/themes.ts — Verfügbare Themes
export const AVAILABLE_THEMES = [
  'default',
  'ocean',
  'forest',
  'sunset',
  'midnight',
  'coral',
  'slate',
  'aurora',
  'crimson',
] as const
Jedes Theme ist für einen bestimmten Anwendungsfall konzipiert:
ThemeFarbeGeeignet für
defaultBlauProfessionelles SaaS, allgemeine Anwendungen
oceanTürkisMaritime Projekte, Tech-Startups, Entwicklerwerkzeuge
forestGrünUmweltfreundliche Produkte, Nachhaltigkeit, Gesundheit & Wellness
sunsetOrangeKreativwerkzeuge, Design-Plattformen, Marktplätze
midnightLilaPremium-Enterprise, Fintech, Luxusprodukte
coralPinkConsumer-Apps, soziale Plattformen, Lifestyle-Marken
slateGrauBusiness-Tools, B2B-Plattformen, Analytics-Dashboards
auroraCyanTech-Plattformen, KI-Produkte, innovative Startups
crimsonRotPerformance-Tools, Analytics, Gaming-Plattformen

Themes wechseln

Um das Theme deiner Anwendung zu ändern, setze die COLOR_THEME-Umgebungsvariable in deiner apps/boilerplate/.env.local:
bash
# apps/boilerplate/.env.local
COLOR_THEME=ocean
Die Funktion getActiveTheme() liest diese Variable und validiert sie gegen die Liste der verfügbaren Themes. Fehlt der Wert oder ist er ungültig, wird auf default zurückgegriffen:
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'
}
Das Root-Layout ruft diese Funktion auf und setzt das Ergebnis als data-theme-Attribut am <html>-Element:
src/app/layout.tsx — Theme-Anwendung
// Get active color theme from environment variable
  const colorTheme = getActiveTheme()

Dark Mode

Der Dark Mode wird unabhängig vom Farbthema verwaltet. Er nutzt next-themes, um eine .dark-Klasse am <html>-Element umzuschalten, was die dunkle Variante des aktuellen Farbthemas aktiviert.

ThemeProvider

Der ThemeProvider umhüllt die Anwendung und verwaltet den Dark-Mode-Zustand. Er erkennt die Systemeinstellung, speichert die Benutzerwahl in localStorage und wendet die .dark-Klasse an:
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>
  )
}
Der Provider wird im Root-Layout innerhalb der Provider-Hierarchie konfiguriert:
src/app/layout.tsx — Provider-Hierarchie mit 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>
                <TooltipProvider>
                  {children}
                  <Toaster
                    richColors
                    position="top-center"
                    duration={8000}
                    toastOptions={{
                      classNames: {
                        icon: '!self-start !mt-0.5',
                        description: '!whitespace-pre-line',
                      },
                    }}
                  />
                  <FilteredAnalytics />
                </TooltipProvider>
              </DemoProvider>
            </ThemeProvider>
          </QueryProvider>
        </MSWProvider>
      </body>
    </html>
  )

ThemeToggle

Die ThemeToggle-Komponente stellt einen Sonne/Mond-Button bereit, mit dem Nutzer zwischen hellem und dunklem Modus wechseln können:
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'

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
        disabled
        className="flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors"
        aria-label="Toggle theme"
      >
        <Moon className="h-4 w-4" />
      </button>
    )
  }

  return (
    <button
      onClick={toggleTheme}
      className="flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:text-foreground"
      aria-label="Toggle theme"
    >
      {resolvedTheme === 'dark' ? (
        <Moon className="h-4 w-4" />
      ) : (
        <Sun className="h-4 w-4" />
      )}
    </button>
  )
}
Wichtige Implementierungsdetails:
  • resolvedTheme wird statt theme verwendet, weil theme den Wert 'system' zurückgeben kann, der keinen Aufschluss über den tatsächlich aufgelösten Modus gibt
  • mounted-State verhindert Hydration-Mismatches — der Toggle rendert während des SSR einen deaktivierten Platzhalter und zeigt die interaktive Version erst nach der clientseitigen Hydration
  • Der Toggle ruft setTheme() von next-themes auf, das die .dark-Klasse am <html>-Element hinzufügt oder entfernt

CSS Custom Properties

Jedes Theme definiert seine Farben als CSS Custom Properties mit HSL-Werten ohne den hsl()-Wrapper. Das ist die Grundlage, die das gesamte Farbsystem mit Tailwinds Opacity-Modifikatoren zum Laufen bringt.
Hier sind die Light-Mode-Variablen des Default-Themes:
src/styles/themes/default.css — Light-Mode-Variablen
[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%;
  --warning: 38 92% 50%;
  --warning-foreground: 0 0% 100%;
  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 221 83% 53%;
  --radius: 0.5rem;
}
Und die Dark-Mode-Variante:
src/styles/themes/default.css — Dark-Mode-Variablen
[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%;
  --warning: 45 93% 47%;
  --warning-foreground: 0 0% 0%;
  --border: 217.2 32.6% 17.5%;
  --input: 217.2 32.6% 17.5%;
  --ring: 217.2 91.2% 59.8%;
}

Variablen-Referenz

VariableZweckVerwendungsbeispiel
--backgroundSeitenhintergrundbg-background
--foregroundPrimäre Textfarbetext-foreground
--cardKarten-/Oberflächen-Hintergrundbg-card
--card-foregroundText auf Kartentext-card-foreground
--primaryMarken-/Akzentfarbebg-primary, text-primary
--primary-foregroundText auf primären Hintergründentext-primary-foreground
--secondarySekundäre Elementebg-secondary
--mutedDezente Hintergründebg-muted
--muted-foregroundGedämpfter Texttext-muted-foreground
--accentHover-/Fokus-Hervorhebungenbg-accent
--destructiveFehler-/Gefahrenzuständebg-destructive
--borderRahmen und Trennlinienborder-border
--inputFormularfeld-Rahmenborder-input
--ringFokus-Ring-Farbering-ring
--radiusBasis-Rahmenradiusrounded-lg, rounded-md

Ein eigenes Theme erstellen

1

CSS-Datei erstellen

Füge eine neue Datei in apps/boilerplate/src/styles/themes/ mit den Farbvariablen deines Themes hinzu. Verwende das Default-Theme als Ausgangspunkt:
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%;
  /* ... alle Variablen definieren */
}

[data-theme='ruby'].dark {
  --background: 346 30% 6%;
  --foreground: 0 0% 98%;
  --primary: 346 77% 59%;
  --primary-foreground: 0 0% 9%;
  /* ... alle Dark-Mode-Variablen definieren */
}
2

Theme registrieren

Füge deinen Theme-Namen zum AVAILABLE_THEMES-Array und zum THEME_METADATA-Objekt in apps/boilerplate/src/styles/themes/themes.ts hinzu:
typescript
export const AVAILABLE_THEMES = [
  'default',
  'ocean',
  // ... bestehende Themes
  'ruby',  // Neues Theme hinzufügen
] as const
3

In globals.css importieren

Füge den Import am Anfang von apps/boilerplate/src/app/globals.css hinzu, neben den anderen Theme-Importen:
css
@import '../styles/themes/ruby.css';
4

Umgebungsvariable setzen

Aktualisiere deine apps/boilerplate/.env.local, um das neue Theme zu verwenden:
bash
COLOR_THEME=ruby
Starte deinen Dev-Server neu und das neue Theme ist aktiv.

Wichtige Dateien

DateiZweck
apps/boilerplate/src/styles/themes/themes.tsTheme-Registry — verfügbare Themes, Metadaten, getActiveTheme()
apps/boilerplate/src/styles/themes/default.cssStandard-Blau-Theme CSS-Variablen (hell + dunkel)
apps/boilerplate/src/styles/themes/*.cssAlle 9 Theme-CSS-Dateien (ocean, forest, sunset, etc.)
apps/boilerplate/src/app/layout.tsxRoot-Layout — setzt das data-theme-Attribut am <html>
apps/boilerplate/src/providers/theme-provider.tsxThemeProvider-Wrapper für next-themes
packages/ui/src/theme-toggle.tsxDark-Mode-Toggle-Button-Komponente
apps/boilerplate/src/app/globals.cssGlobale Stile — importiert alle Theme-CSS-Dateien