We use cookies to enhance your experience on the site
CodeWorlds

Dark Mode and User Preferences in Next.js 15

In Metropolis Quantum, the city adapts to the needs of its residents. During the day, the glass facades of buildings let in natural sunlight, and holographic displays operate in high-brightness mode. When dusk falls, the city automatically switches to night mode - lighting becomes warmer and less intense, holographic displays switch to dark mode to avoid straining residents' eyes.

In modern web applications, a similar mechanism - Dark Mode - has become not just an aesthetic element but a standard of usability and accessibility. In this chapter, we will learn ways to implement dark mode and manage user preferences in Next.js 15 applications.

Importance of Dark Mode and User Preferences

Why is Dark Mode Important?

  1. Comfort of use - reduces eye fatigue, especially in low-light conditions
  2. Energy savings - on devices with OLED/AMOLED screens, dark mode can significantly extend battery life
  3. Accessibility - helps users with light sensitivity or certain vision impairments
  4. Aesthetics - many people prefer dark interfaces for aesthetic reasons
  5. System compatibility - maintains consistency with the user's operating system preferences

Other User Preferences

In addition to dark mode, it is also worth considering other preferences:

  1. Text size - for improved readability
  2. Contrast - some users need higher contrast
  3. Animations - option to reduce animations for people with vestibular disorders
  4. Preferred language - support for multiple languages
  5. Layout - layout change options (e.g., content density)

Implementacja Dark Mode w Next.js 15

Next.js 15 offers several approaches to implementing dark mode:

1. CSS i zmienne CSS

The simplest approach uses CSS variables and class selectors:

1/* app/globals.css */
2:root {
3  /* Zmienne dla trybu jasnego */
4  --background: #ffffff;
5  --foreground: #121212;
6  --primary: #0070f3;
7  --secondary: #6b7280;
8  --accent: #8b5cf6;
9  --muted: #f3f4f6;
10  --border: #e5e7eb;
11}
12
13/* Klasa dla trybu ciemnego */
14.dark-theme {
15  --background: #121212;
16  --foreground: #f3f4f6;
17  --primary: #3b82f6;
18  --secondary: #9ca3af;
19  --accent: #a78bfa;
20  --muted: #1f2937;
21  --border: #374151;
22}
23
24body {
25  background-color: var(--background);
26  color: var(--foreground);
27  transition: background-color 0.3s ease, color 0.3s ease;
28}
29
30.card {
31  background-color: var(--muted);
32  border: 1px solid var(--border);
33  color: var(--foreground);
34}
35
36.button-primary {
37  background-color: var(--primary);
38  color: white;
39}

2. Theme Toggle Component

1// components/ThemeToggle.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { MoonIcon, SunIcon } from '@/components/Icons';
6import styles from './ThemeToggle.module.css';
7
8export function ThemeToggle() {
9  // Start with a neutral value to avoid flash during hydration
10  const [isDarkMode, setIsDarkMode] = useState<boolean | undefined>(undefined);
11  
12  useEffect(() => {
13    // Check system preferences
14    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
15    
16    // Check saved user preferences
17    const savedTheme = localStorage.getItem('theme');
18    const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
19    
20    setIsDarkMode(initialTheme === 'dark');
21    document.documentElement.classList.toggle('dark-theme', initialTheme === 'dark');
22  }, []);
23  
24  // Efekt uruchamiany przy zmianie stanu
25  useEffect(() => {
26    if (isDarkMode === undefined) return;
27    
28    document.documentElement.classList.toggle('dark-theme', isDarkMode);
29    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
30  }, [isDarkMode]);
31  
32  // Do not render the button before determining initial state
33  if (isDarkMode === undefined) return null;
34  
35  return (
36    <button
37      className={styles.themeToggle}
38      onClick={() => setIsDarkMode(!isDarkMode)}
39      aria-label={isDarkMode ? 'Switch to light theme' : 'Switch to dark theme'}
40    >
41      {isDarkMode ? <SunIcon /> : <MoonIcon />}
42    </button>
43  );
44}
1/* components/ThemeToggle.module.css */
2.themeToggle {
3  background: none;
4  border: none;
5  width: 40px;
6  height: 40px;
7  border-radius: 50%;
8  display: flex;
9  align-items: center;
10  justify-content: center;
11  cursor: pointer;
12  color: var(--foreground);
13  transition: background-color 0.3s ease;
14}
15
16.themeToggle:hover {
17  background-color: var(--muted);
18}

3. Context Provider for Global Theme Access

1// contexts/ThemeContext.tsx
2'use client';
3
4import { createContext, useState, useEffect, useContext, ReactNode } from 'react';
5
6type Theme = 'light' | 'dark';
7
8interface ThemeContextType {
9  theme: Theme;
10  setTheme: (theme: Theme) => void;
11  toggleTheme: () => void;
12}
13
14const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
15
16export function ThemeProvider({ children }: { children: ReactNode }) {
17  const [theme, setTheme] = useState<Theme>('light');
18  
19  // Initialize theme based on system preferences and saved settings
20  useEffect(() => {
21    // Check if we are in a browser environment
22    if (typeof window !== 'undefined') {
23      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
24      const savedTheme = localStorage.getItem('theme') as Theme | null;
25      
26      const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
27      setTheme(initialTheme);
28      document.documentElement.classList.toggle('dark-theme', initialTheme === 'dark');
29    }
30  }, []);
31  
32  // Efekt uruchamiany przy zmianie tematu
33  useEffect(() => {
34    if (typeof window !== 'undefined') {
35      document.documentElement.classList.toggle('dark-theme', theme === 'dark');
36      localStorage.setItem('theme', theme);
37    }
38  }, [theme]);
39  
40  // Function to toggle theme
41  const toggleTheme = () => {
42    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
43  };
44  
45  return (
46    <ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
47      {children}
48    </ThemeContext.Provider>
49  );
50}
51
52// Hook to use theme context
53export function useTheme() {
54  const context = useContext(ThemeContext);
55  if (context === undefined) {
56    throw new Error('useTheme must be used within a ThemeProvider');
57  }
58  return context;
59}

4. Implementacja w root layout

1// app/layout.tsx
2import { ThemeProvider } from '@/contexts/ThemeContext';
3import './globals.css';
4import type { Metadata } from 'next';
5
6export const metadata: Metadata = {
7  title: 'Metropolis Quantum',
8  description: 'The city of the future at your fingertips',
9};
10
11export default function RootLayout({
12  children,
13}: {
14  children: React.ReactNode;
15}) {
16  return (
17    <html lang="pl">
18      <body>
19        <ThemeProvider>
20          {children}
21        </ThemeProvider>
22      </body>
23    </html>
24  );
25}

5. Using Context in Components

1// components/Header.tsx
2'use client';
3
4import { useTheme } from '@/contexts/ThemeContext';
5import Link from 'next/link';
6import styles from './Header.module.css';
7
8export function Header() {
9  const { theme, toggleTheme } = useTheme();
10  
11  return (
12    <header className={styles.header}>
13      <div className={styles.logo}>
14        <Link href="/">Metropolis Quantum</Link>
15      </div>
16      <nav className={styles.nav}>
17        <ul>
18          <li><Link href="/about">O nas</Link></li>
19          <li><Link href="/services">Services</Link></li>
20          <li><Link href="/contact">Kontakt</Link></li>
21        </ul>
22      </nav>
23      <button 
24        className={styles.themeToggle}
25        onClick={toggleTheme}
26        aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
27      >
28        {theme === 'dark' ? '☀️' : '🌙'}
29      </button>
30    </header>
31  );
32}

Zaawansowane implementacje Dark Mode

1. Using next-themes

next-themes
is a popular library that simplifies dark mode implementation:

1npm install next-themes
1// app/providers.tsx
2'use client';
3
4import { ThemeProvider } from 'next-themes';
5
6export function Providers({ children }: { children: React.ReactNode }) {
7  return (
8    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
9      {children}
10    </ThemeProvider>
11  );
12}
1// app/layout.tsx
2import { Providers } from './providers';
3import './globals.css';
4import type { Metadata } from 'next';
5
6export const metadata: Metadata = {
7  title: 'Metropolis Quantum',
8  description: 'The city of the future at your fingertips',
9};
10
11export default function RootLayout({
12  children,
13}: {
14  children: React.ReactNode;
15}) {
16  return (
17    <html lang="pl" suppressHydrationWarning>
18      <body>
19        <Providers>{children}</Providers>
20      </body>
21    </html>
22  );
23}
1/* app/globals.css */
2:root {
3  --background: #ffffff;
4  --foreground: #121212;
5  /* Inne zmienne dla jasnego motywu */
6}
7
8.dark {
9  --background: #121212;
10  --foreground: #f3f4f6;
11  /* Inne zmienne dla ciemnego motywu */
12}
13
14body {
15  background-color: var(--background);
16  color: var(--foreground);
17  transition: background-color 0.3s ease, color 0.3s ease;
18}
1// components/ThemeSwitch.tsx
2'use client';
3
4import { useTheme } from 'next-themes';
5import { useEffect, useState } from 'react';
6
7export function ThemeSwitch() {
8  const [mounted, setMounted] = useState(false);
9  const { theme, setTheme } = useTheme();
10  
11  // Efekt uruchamiany po montowaniu komponentu
12  useEffect(() => {
13    setMounted(true);
14  }, []);
15  
16  if (!mounted) {
17    return null; // Nie renderuj podczas pierwszego renderowania (SSR)
18  }
19  
20  return (
21    <select
22      value={theme}
23      onChange={(e) => setTheme(e.target.value)}
24      className="bg-transparent px-4 py-2 rounded border border-gray-300 dark:border-gray-700"
25    >
26      <option value="system">Ustawienia systemowe</option>
27      <option value="light">Jasny</option>
28      <option value="dark">Ciemny</option>
29    </select>
30  );
31}

2. Support for Multiple Themes (Not Just Light/Dark)

1// contexts/ThemeContext.tsx
2'use client';
3
4import { createContext, useState, useEffect, useContext, ReactNode } from 'react';
5
6type Theme = 'light' | 'dark' | 'sepia' | 'forest' | 'ocean';
7
8interface ThemeContextType {
9  theme: Theme;
10  setTheme: (theme: Theme) => void;
11}
12
13const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
14
15export function ThemeProvider({ children }: { children: ReactNode }) {
16  const [theme, setTheme] = useState<Theme>('light');
17  
18  // Inicjalizacja tematu
19  useEffect(() => {
20    if (typeof window !== 'undefined') {
21      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
22      const savedTheme = localStorage.getItem('theme') as Theme | null;
23      
24      const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
25      setTheme(initialTheme);
26      
27      // Remove all theme classes
28      document.documentElement.classList.remove('theme-light', 'theme-dark', 'theme-sepia', 'theme-forest', 'theme-ocean');
29      // Add current theme class
30      document.documentElement.classList.add(`theme-${initialTheme}`);
31    }
32  }, []);
33  
34  // Efekt uruchamiany przy zmianie tematu
35  useEffect(() => {
36    if (typeof window !== 'undefined') {
37      // Remove all theme classes
38      document.documentElement.classList.remove('theme-light', 'theme-dark', 'theme-sepia', 'theme-forest', 'theme-ocean');
39      // Add current theme class
40      document.documentElement.classList.add(`theme-${theme}`);
41      localStorage.setItem('theme', theme);
42    }
43  }, [theme]);
44  
45  return (
46    <ThemeContext.Provider value={{ theme, setTheme }}>
47      {children}
48    </ThemeContext.Provider>
49  );
50}
51
52export function useTheme() {
53  const context = useContext(ThemeContext);
54  if (context === undefined) {
55    throw new Error('useTheme must be used within a ThemeProvider');
56  }
57  return context;
58}
1/* app/globals.css */
2:root {
3  /* Podstawowe zmienne */
4}
5
6/* Light theme (default) */
7.theme-light {
8  --background: #ffffff;
9  --foreground: #121212;
10  --primary: #0070f3;
11  --secondary: #6b7280;
12  --accent: #8b5cf6;
13  --muted: #f3f4f6;
14  --border: #e5e7eb;
15}
16
17/* Ciemny motyw */
18.theme-dark {
19  --background: #121212;
20  --foreground: #f3f4f6;
21  --primary: #3b82f6;
22  --secondary: #9ca3af;
23  --accent: #a78bfa;
24  --muted: #1f2937;
25  --border: #374151;
26}
27
28/* Motyw sepia (przyjazny dla oczu) */
29.theme-sepia {
30  --background: #f9f5eb;
31  --foreground: #433422;
32  --primary: #9c6644;
33  --secondary: #7d6e5d;
34  --accent: #c27b57;
35  --muted: #efe8d9;
36  --border: #d3c6ad;
37}
38
39/* Forest theme */
40.theme-forest {
41  --background: #1e2a20;
42  --foreground: #e9f0db;
43  --primary: #52b788;
44  --secondary: #74c69d;
45  --accent: #95d5b2;
46  --muted: #2d3a2e;
47  --border: #40534e;
48}
49
50/* Motyw oceaniczny */
51.theme-ocean {
52  --background: #0c2333;
53  --foreground: #e0f2fe;
54  --primary: #0ea5e9;
55  --secondary: #38bdf8;
56  --accent: #7dd3fc;
57  --muted: #164e63;
58  --border: #0369a1;
59}
60
61body {
62  background-color: var(--background);
63  color: var(--foreground);
64  transition: background-color 0.3s ease, color 0.3s ease;
65}

3. Animated Transitions Between Themes

1/* app/globals.css */
2body {
3  background-color: var(--background);
4  color: var(--foreground);
5  transition: 
6    background-color 0.5s ease, 
7    color 0.5s ease,
8    border-color 0.5s ease,
9    box-shadow 0.5s ease;
10}
11
12/* Animation for nested elements */
13* {
14  transition: 
15    background-color 0.5s ease, 
16    color 0.5s ease,
17    border-color 0.5s ease,
18    fill 0.5s ease,
19    stroke 0.5s ease;
20}

4. Zastosowanie tailwindcss dla trybu ciemnego

Tailwind CSS ma wbudowane wsparcie dla trybu ciemnego:

1// components/Card.tsx
2export function Card({ title, content }) {
3  return (
4    <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md dark:shadow-gray-900 border border-gray-200 dark:border-gray-700">
5      <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{title}</h3>
6      <p className="text-gray-700 dark:text-gray-300">{content}</p>
7    </div>
8  );
9}

Saving and Managing User Preferences

1. Using localStorage

1// utils/userPreferences.ts
2export interface UserPreferences {
3  theme: 'light' | 'dark' | 'system';
4  fontSize: 'small' | 'medium' | 'large';
5  reducedMotion: boolean;
6  highContrast: boolean;
7  language: string;
8}
9
10const defaultPreferences: UserPreferences = {
11  theme: 'system',
12  fontSize: 'medium',
13  reducedMotion: false,
14  highContrast: false,
15  language: 'pl',
16};
17
18export function getUserPreferences(): UserPreferences {
19  if (typeof window === 'undefined') {
20    return defaultPreferences;
21  }
22  
23  try {
24    const savedPreferences = localStorage.getItem('userPreferences');
25    return savedPreferences
26      ? { ...defaultPreferences, ...JSON.parse(savedPreferences) }
27      : defaultPreferences;
28  } catch (error) {
29    console.error('Error reading user preferences:', error);
30    return defaultPreferences;
31  }
32}
33
34export function saveUserPreferences(preferences: Partial<UserPreferences>): void {
35  if (typeof window === 'undefined') {
36    return;
37  }
38  
39  try {
40    const currentPreferences = getUserPreferences();
41    const updatedPreferences = { ...currentPreferences, ...preferences };
42    localStorage.setItem('userPreferences', JSON.stringify(updatedPreferences));
43  } catch (error) {
44    console.error('Error saving user preferences:', error);
45  }
46}

2. User Preferences with Cookies

Cookies can be used to store preferences between sessions and are available on both client and server:

1// lib/cookies.ts
2import { parseCookies, setCookie } from 'nookies';
3import { UserPreferences, defaultPreferences } from './userPreferences';
4
5export function getUserPreferencesFromCookies(ctx?: any): UserPreferences {
6  const cookies = parseCookies(ctx);
7  
8  if (!cookies.userPreferences) {
9    return defaultPreferences;
10  }
11  
12  try {
13    return { ...defaultPreferences, ...JSON.parse(cookies.userPreferences) };
14  } catch (error) {
15    console.error('Error parsing user preferences from cookies:', error);
16    return defaultPreferences;
17  }
18}
19
20export function saveUserPreferencesToCookies(
21  preferences: Partial<UserPreferences>,
22  ctx?: any
23): void {
24  const currentPreferences = getUserPreferencesFromCookies(ctx);
25  const updatedPreferences = { ...currentPreferences, ...preferences };
26  
27  setCookie(ctx, 'userPreferences', JSON.stringify(updatedPreferences), {
28    maxAge: 365 * 24 * 60 * 60, // 1 rok
29    path: '/',
30    sameSite: 'strict',
31    secure: process.env.NODE_ENV === 'production',
32  });
33}

3. Switches for Different Preferences

1// components/UserPreferencesPanel.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { UserPreferences, getUserPreferences, saveUserPreferences } from '@/utils/userPreferences';
6import styles from './UserPreferencesPanel.module.css';
7
8export function UserPreferencesPanel() {
9  const [preferences, setPreferences] = useState<UserPreferences | null>(null);
10  const [isOpen, setIsOpen] = useState(false);
11  
12  useEffect(() => {
13    setPreferences(getUserPreferences());
14  }, []);
15  
16  if (!preferences) return null;
17  
18  const updatePreference = <K extends keyof UserPreferences>(
19    key: K,
20    value: UserPreferences[K]
21  ) => {
22    const updatedPreferences = { ...preferences, [key]: value };
23    setPreferences(updatedPreferences);
24    saveUserPreferences({ [key]: value });
25    
26    // Zastosuj preferencje natychmiast
27    applyPreferences(key, value);
28  };
29  
30  const applyPreferences = <K extends keyof UserPreferences>(
31    key: K,
32    value: UserPreferences[K]
33  ) => {
34    switch (key) {
35      case 'theme':
36        const theme = value as 'light' | 'dark' | 'system';
37        const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
38        const effectiveTheme = theme === 'system' ? (prefersDark ? 'dark' : 'light') : theme;
39        
40        document.documentElement.classList.remove('theme-light', 'theme-dark');
41        document.documentElement.classList.add(`theme-${effectiveTheme}`);
42        break;
43        
44      case 'fontSize':
45        document.documentElement.style.fontSize = {
46          small: '14px',
47          medium: '16px',
48          large: '18px',
49        }[value as string] || '16px';
50        break;
51        
52      case 'reducedMotion':
53        document.documentElement.classList.toggle('reduce-motion', value as boolean);
54        break;
55        
56      case 'highContrast':
57        document.documentElement.classList.toggle('high-contrast', value as boolean);
58        break;
59        
60      case 'language':
61        // In a real application - here you would implement language change
62        break;
63    }
64  };
65  
66  return (
67    <>
68      <button 
69        className={styles.settingsButton}
70        onClick={() => setIsOpen(!isOpen)}
71        aria-expanded={isOpen}
72      >
73        ⚙️ Preferencje
74      </button>
75      
76      {isOpen && (
77        <div className={styles.panel}>
78          <h2>User Preferences</h2>
79          
80          <div className={styles.preferenceGroup}>
81            <h3>Motyw</h3>
82            <div className={styles.options}>
83              <label>
84                <input
85                  type="radio"
86                  name="theme"
87                  value="light"
88                  checked={preferences.theme === 'light'}
89                  onChange={() => updatePreference('theme', 'light')}
90                />
91                Jasny
92              </label>
93              
94              <label>
95                <input
96                  type="radio"
97                  name="theme"
98                  value="dark"
99                  checked={preferences.theme === 'dark'}
100                  onChange={() => updatePreference('theme', 'dark')}
101                />
102                Ciemny
103              </label>
104              
105              <label>
106                <input
107                  type="radio"
108                  name="theme"
109                  value="system"
110                  checked={preferences.theme === 'system'}
111                  onChange={() => updatePreference('theme', 'system')}
112                />
113                Zgodny z systemem
114              </label>
115            </div>
116          </div>
117          
118          <div className={styles.preferenceGroup}>
119            <h3>Rozmiar tekstu</h3>
120            <div className={styles.options}>
121              <label>
122                <input
123                  type="radio"
124                  name="fontSize"
125                  value="small"
126                  checked={preferences.fontSize === 'small'}
127                  onChange={() => updatePreference('fontSize', 'small')}
128                />
129                Small
130              </label>
131              
132              <label>
133                <input
134                  type="radio"
135                  name="fontSize"
136                  value="medium"
137                  checked={preferences.fontSize === 'medium'}
138                  onChange={() => updatePreference('fontSize', 'medium')}
139                />
140                Medium
141              </label>
142              
143              <label>
144                <input
145                  type="radio"
146                  name="fontSize"
147                  value="large"
148                  checked={preferences.fontSize === 'large'}
149                  onChange={() => updatePreference('fontSize', 'large')}
150                />
151                Large
152              </label>
153            </div>
154          </div>
155          
156          <div className={styles.preferenceGroup}>
157            <h3>Accessibility</h3>
158            <div className={styles.options}>
159              <label>
160                <input
161                  type="checkbox"
162                  checked={preferences.reducedMotion}
163                  onChange={(e) => updatePreference('reducedMotion', e.target.checked)}
164                />
165                Zredukowane animacje
166              </label>
167              
168              <label>
169                <input
170                  type="checkbox"
171                  checked={preferences.highContrast}
172                  onChange={(e) => updatePreference('highContrast', e.target.checked)}
173                />
174                Wysoki kontrast
175              </label>
176            </div>
177          </div>
178          
179          <div className={styles.preferenceGroup}>
180            <h3>Language</h3>
181            <select
182              value={preferences.language}
183              onChange={(e) => updatePreference('language', e.target.value)}
184              className={styles.select}
185            >
186              <option value="pl">Polski</option>
187              <option value="en">English</option>
188              <option value="de">Deutsch</option>
189              <option value="fr">Français</option>
190            </select>
191          </div>
192          
193          <button 
194            className={styles.closeButton}
195            onClick={() => setIsOpen(false)}
196          >
197            Zamknij
198          </button>
199        </div>
200      )}
201    </>
202  );
203}

CSS for Different Preferences

1. CSS Variables for Different Preferences

1/* app/globals.css */
2:root {
3  /* Podstawowe zmienne */
4  --background: #ffffff;
5  --foreground: #121212;
6  /* ... (inne podstawowe kolory) ... */
7  
8  /* Zmienne dla rozmiaru tekstu */
9  --font-size-base: 16px;
10  --font-size-small: 0.875rem;
11  --font-size-medium: 1rem;
12  --font-size-large: 1.25rem;
13  --font-size-xl: 1.5rem;
14  --font-size-2xl: 2rem;
15  
16  /* Variables for spacing */
17  --spacing-xs: 0.25rem;
18  --spacing-sm: 0.5rem;
19  --spacing-md: 1rem;
20  --spacing-lg: 1.5rem;
21  --spacing-xl: 2rem;
22  
23  /* Zmienne dla animacji */
24  --transition-speed: 0.3s;
25}
26
27/* Tryb ciemny */
28.theme-dark {
29  --background: #121212;
30  --foreground: #f3f4f6;
31  /* ... (inne kolory dla trybu ciemnego) ... */
32}
33
34/* Wysoki kontrast */
35.high-contrast {
36  --background: #000000;
37  --foreground: #ffffff;
38  --primary: #ffff00;
39  --secondary: #00ffff;
40  --accent: #ff00ff;
41  --muted: #333333;
42  --border: #ffffff;
43}
44
45/* Large text */
46html[data-text-size="large"] {
47  --font-size-base: 18px;
48  --font-size-small: 1rem;
49  --font-size-medium: 1.125rem;
50  --font-size-large: 1.375rem;
51  --font-size-xl: 1.75rem;
52  --font-size-2xl: 2.25rem;
53}
54
55/* Zredukowane animacje */
56.reduce-motion * {
57  animation-duration: 0.001s !important;
58  transition-duration: 0.001s !important;
59  scroll-behavior: auto !important;
60}
61
62/* Podstawowe style */
63html {
64  font-size: var(--font-size-base);
65}
66
67body {
68  background-color: var(--background);
69  color: var(--foreground);
70  font-size: var(--font-size-medium);
71  transition: 
72    background-color var(--transition-speed) ease,
73    color var(--transition-speed) ease;
74}
75
76a {
77  color: var(--primary);
78  transition: color var(--transition-speed) ease;
79}
80
81.reduce-motion a {
82  transition: none;
83}

2. Component Heights for Different Text Sizes

1/* components/Button.module.css */
2.button {
3  background-color: var(--primary);
4  color: white;
5  border-radius: 0.375rem;
6  font-weight: 500;
7  padding: var(--spacing-sm) var(--spacing-md);
8  font-size: var(--font-size-medium);
9  height: calc(var(--font-size-medium) * 2.5);
10  line-height: 1;
11  display: inline-flex;
12  align-items: center;
13  justify-content: center;
14  transition: background-color var(--transition-speed) ease;
15}
16
17.button:hover {
18  background-color: var(--primary-dark);
19}
20
21.reduce-motion .button {
22  transition: none;
23}

Implementation for Advanced Cases

1. Tryb ciemny z przechowywaniem na serwerze

For applications with authentication, user preferences can be stored on the server:

1// app/api/preferences/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '@/app/api/auth/[...nextauth]/route';
5import { prisma } from '@/lib/prisma';
6
7export async function GET(req: NextRequest) {
8  const session = await getServerSession(authOptions);
9  
10  if (!session || !session.user) {
11    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12  }
13  
14  try {
15    const preferences = await prisma.userPreferences.findUnique({
16      where: { userId: session.user.id },
17    });
18    
19    return NextResponse.json(preferences);
20  } catch (error) {
21    return NextResponse.json({ error: 'Failed to fetch preferences' }, { status: 500 });
22  }
23}
24
25export async function POST(req: NextRequest) {
26  const session = await getServerSession(authOptions);
27  
28  if (!session || !session.user) {
29    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
30  }
31  
32  try {
33    const data = await req.json();
34    const preferences = await prisma.userPreferences.upsert({
35      where: { userId: session.user.id },
36      update: data,
37      create: {
38        userId: session.user.id,
39        ...data,
40      },
41    });
42    
43    return NextResponse.json(preferences);
44  } catch (error) {
45    return NextResponse.json({ error: 'Failed to update preferences' }, { status: 500 });
46  }
47}

2. SSR and Pre-rendering with Preference Support

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7  
8  // If this is the first request, check if a preference cookie exists
9  const hasPrefsCookie = request.cookies.has('userPreferences');
10  
11  // If there is no cookie and this is the first HTML rendering,
12  // add a script detecting system preferences
13  if (!hasPrefsCookie && request.headers.get('sec-fetch-dest') === 'document') {
14    const url = request.nextUrl.clone();
15    
16    // If this is an HTML page request and not API or static assets
17    if (!url.pathname.startsWith('/api/') && !url.pathname.match(/.(js|css|png|jpg|svg)$/)) {
18      const response = NextResponse.next();
19      
20      // In a real implementation you can add a script detecting preferences 
21      // and setting appropriate cookies before the first rendering
22      
23      return response;
24    }
25  }
26  
27  return response;
28}
29
30export const config = {
31  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
32};

3. Automatic Dark Mode Switching Based on Time of Day

1// hooks/useDayNightMode.ts
2import { useState, useEffect } from 'react';
3
4export function useDayNightMode() {
5  const [isDarkMode, setIsDarkMode] = useState(false);
6  
7  useEffect(() => {
8    // Function checking if it is currently night (from 18:00 to 6:00)
9    const checkIfNight = () => {
10      const hours = new Date().getHours();
11      return hours >= 18 || hours < 6;
12    };
13    
14    // Initial setting
15    setIsDarkMode(checkIfNight());
16    
17    // Interval checking time every minute
18    const interval = setInterval(() => {
19      setIsDarkMode(checkIfNight());
20    }, 60000);
21    
22    return () => clearInterval(interval);
23  }, []);
24  
25  return isDarkMode;
26}
1// components/AutomaticThemeToggle.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { useDayNightMode } from '@/hooks/useDayNightMode';
6import styles from './AutomaticThemeToggle.module.css';
7
8export function AutomaticThemeToggle() {
9  const [isAutoMode, setIsAutoMode] = useState(false);
10  const isDarkMode = useDayNightMode();
11  const [manualDarkMode, setManualDarkMode] = useState(false);
12  
13  // Effect changing mode based on automatic detection or manual setting
14  useEffect(() => {
15    if (isAutoMode) {
16      document.documentElement.classList.toggle('dark-theme', isDarkMode);
17    } else {
18      document.documentElement.classList.toggle('dark-theme', manualDarkMode);
19    }
20  }, [isAutoMode, isDarkMode, manualDarkMode]);
21  
22  return (
23    <div className={styles.themeControls}>
24      <div className={styles.autoToggle}>
25        <label>
26          <input
27            type="checkbox"
28            checked={isAutoMode}
29            onChange={(e) => setIsAutoMode(e.target.checked)}
30          />
31          Automatyczny tryb dzienny/nocny
32        </label>
33      </div>
34      
35      {!isAutoMode && (
36        <div className={styles.manualToggle}>
37          <button
38            onClick={() => setManualDarkMode(!manualDarkMode)}
39            className={styles.themeButton}
40          >
41            {manualDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
42          </button>
43        </div>
44      )}
45      
46      <div className={styles.currentMode}>
47        Aktualny tryb: {(isAutoMode ? isDarkMode : manualDarkMode) ? 'Ciemny' : 'Jasny'}
48      </div>
49    </div>
50  );
51}

Best Practices

  1. Respect system preferences - always check
    prefers-color-scheme
    as the default setting
  2. Avoid flashes - prevent content flickering when switching modes
  3. Consistent colors - make sure all interface elements properly respond to mode changes
  4. Test thoroughly - check how the application looks in both modes
  5. Accessibility - remember that dark mode is not just about appearance but also accessibility
  6. Performance - avoid multiple class and style rewrites when switching
  7. Signal active mode - clearly indicate which mode is currently active
  8. Save preferences - save selected user preferences between sessions

Implementacja z TailwindCSS

Tailwind CSS integrates excellently with dark mode:

1// components/DarkModeExample.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5
6export function DarkModeExample() {
7  const [darkMode, setDarkMode] = useState(false);
8
9  useEffect(() => {
10    // Check system preferences
11    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
12    const savedTheme = localStorage.getItem('theme');
13    const initialDarkMode = savedTheme === 'dark' || (!savedTheme && prefersDark);
14    
15    setDarkMode(initialDarkMode);
16    document.documentElement.classList.toggle('dark', initialDarkMode);
17  }, []);
18  
19  const toggleDarkMode = () => {
20    const newDarkMode = !darkMode;
21    setDarkMode(newDarkMode);
22    document.documentElement.classList.toggle('dark', newDarkMode);
23    localStorage.setItem('theme', newDarkMode ? 'dark' : 'light');
24  };
25  
26  return (
27    <div className="p-8 bg-white dark:bg-gray-900 min-h-screen transition-colors duration-300">
28      <div className="max-w-4xl mx-auto">
29        <div className="flex justify-between items-center mb-8">
30          <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
31            Metropolis Quantum
32          </h1>
33          
34          <button
35            onClick={toggleDarkMode}
36            className="p-2 rounded-full bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-white transition-colors duration-300"
37          >
38            {darkMode ? '☀️' : '🌙'}
39          </button>
40        </div>
41        
42        <div className="grid gap-6 md:grid-cols-2">
43          <div className="bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md dark:shadow-gray-800/30">
44            <h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-4">
45              About the City
46            </h2>
47            <p className="text-gray-700 dark:text-gray-300">
48              Metropolis Quantum is the city of the future where technology and humanity coexist in harmony.
49            </p>
50          </div>
51          
52          <div className="bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md dark:shadow-gray-800/30">
53            <h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-4">
54              Services
55            </h2>
56            <ul className="space-y-2 text-gray-700 dark:text-gray-300">
57              <li className="flex items-center">
58                <svg className="w-5 h-5 mr-2 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
59                  <path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" />
60                </svg>
61                Transport kwantowy
62              </li>
63              <li className="flex items-center">
64                <svg className="w-5 h-5 mr-2 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
65                  <path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" />
66                </svg>
67                Holographic Displays
68              </li>
69              <li className="flex items-center">
70                <svg className="w-5 h-5 mr-2 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
71                  <path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" />
72                </svg>
73                Inteligentna infrastruktura
74              </li>
75            </ul>
76          </div>
77        </div>
78        
79        <div className="mt-8 bg-blue-600 dark:bg-blue-800 p-6 rounded-lg text-white shadow-lg">
80          <h2 className="text-xl font-semibold mb-4">Join the Community</h2>
81          <p className="mb-4">
82            Discover the amazing possibilities that Metropolis Quantum offers.
83          </p>
84          <button className="px-4 py-2 bg-white text-blue-600 dark:bg-gray-800 dark:text-blue-400 rounded-md font-medium hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-300">
85            Sign Up
86          </button>
87        </div>
88      </div>
89    </div>
90  );
91}

Summary

Effective implementation of dark mode and managing user preferences in Next.js 15 is an important element of user experience. Just as Quantum Metropolis adapts its systems to the time of day and residents' preferences, modern web applications should respect user choices while also caring about accessibility, performance, and interface consistency.

Key takeaways:

  1. Respect system preferences - many users already have their preferred mode set in the system
  2. Provide a simple toggle - enable easy switching between modes
  3. Save user choices - remember preferences between sessions
  4. Design for both modes - consider the specifics of both modes at the design stage
  5. Pay attention to accessibility - dark mode is not just about aesthetics but also accessibility
  6. Integrate with other preferences - allow users to customize other aspects of the interface as well

Thanks to modern tools such as CSS variables, React's context API, and libraries like next-themes, implementing dark mode and managing user preferences becomes simpler and more effective. Next.js 15 provides a solid foundation for creating adaptive interfaces that respond to user preferences and deliver optimal experiences regardless of conditions.

Go to CodeWorlds