Cours Next.js · Module 3: Styles et optimisation visuelle

Mode sombre et préférences utilisateur dans Next.js 16

20 min de lecture
Dans cette leçon9

À Metropolis Quantum, la ville s'adapte aux besoins de ses habitants. Le jour, les façades vitrées des bâtiments laissent entrer la lumière naturelle du soleil, et les affichages holographiques fonctionnent en mode haute luminosité. À la tombée de la nuit, la ville bascule automatiquement en mode nocturne : l'éclairage devient plus chaud et moins intense, et les affichages holographiques passent en mode sombre pour ne pas fatiguer les yeux des habitants.

Dans les applications web modernes, un mécanisme similaire - le mode sombre - est devenu non seulement un élément esthétique, mais un standard d'utilisabilité et d'accessibilité. Dans ce chapitre, nous découvrirons des façons d'implémenter le mode sombre et de gérer les préférences utilisateur dans les applications Next.js 16.

L'importance du mode sombre et des préférences utilisateur

Pourquoi le mode sombre est-il important ?

  1. Le confort d'utilisation - réduit la fatigue oculaire, surtout en conditions de faible luminosité
  2. L'économie d'énergie - sur les appareils à écran OLED/AMOLED, le mode sombre peut considérablement prolonger l'autonomie de la batterie
  3. L'accessibilité - aide les utilisateurs sensibles à la lumière ou présentant certains troubles de la vision
  4. L'esthétique - beaucoup de personnes préfèrent les interfaces sombres pour des raisons esthétiques
  5. La compatibilité système - maintient la cohérence avec les préférences du système d'exploitation de l'utilisateur

Autres préférences utilisateur

En plus du mode sombre, il vaut aussi la peine de prendre en compte d'autres préférences :

  1. La taille du texte - pour une meilleure lisibilité
  2. Le contraste - certains utilisateurs ont besoin d'un contraste plus élevé
  3. Les animations - une option pour réduire les animations pour les personnes ayant des troubles vestibulaires
  4. La langue préférée - la prise en charge de plusieurs langues
  5. La mise en page - des options de changement de mise en page (par ex. la densité du contenu)

Implémenter le mode sombre dans Next.js 16

Next.js 16 offre plusieurs approches pour implémenter le mode sombre :

1. CSS et variables CSS

L'approche la plus simple utilise des variables CSS et des sélecteurs de classe :

1/* app/globals.css */
2:root {
3  /* Variables pour le thème clair */
4  --background: #ffffff;
5  --foreground: #121212;
6  --primary: #0070f3;
7  --secondary: #6b7280;
8  --accent: #8b5cf6;
9  --muted: #f3f4f6;
10  --border: #e5e7eb;
11}
12
13/* Classe pour le thème sombre */
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. Composant de bascule de thème

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  // Effet déclenché lors du changement d’état
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 pour un accès global au thème

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  // Effet déclenché lors du changement de thème
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. Implémentation dans le 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. Utiliser le contexte dans les composants

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' ? 'Clair' : 'Sombre'}
29      </button>
30    </header>
31  );
32}

Implémentations avancées du mode sombre

1. Utiliser next-themes

next-themes est une bibliothèque populaire qui simplifie l'implémentation du mode sombre :

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  /* Autres variables pour le thème clair */
6}
7
8.dark {
9  --background: #121212;
10  --foreground: #f3f4f6;
11  /* Autres variables pour le thème sombre */
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; // Ne pas rendre lors du premier rendu (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. Prise en charge de plusieurs thèmes (pas seulement clair/sombre)

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  // Effet déclenché lors du changement de thème
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  /* Variables de base */
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/* Thème sépia (agréable pour les yeux) */
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. Transitions animées entre les thèmes

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. Utiliser tailwindcss pour le mode sombre

Tailwind CSS dispose d'une prise en charge intégrée du mode sombre :

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}

Sauvegarder et gérer les préférences utilisateur

1. Utiliser le 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. Préférences utilisateur avec les cookies

Les cookies peuvent servir à stocker les préférences entre les sessions et sont disponibles à la fois côté client et côté serveur :

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. Interrupteurs pour différentes préférences

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>Taille du texte</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 pour différentes préférences

1. Variables CSS pour différentes préférences

1/* app/globals.css */
2:root {
3  /* Variables de base */
4  --background: #ffffff;
5  --foreground: #121212;
6  /* ... (inne podstawowe kolory) ... */
7
8  /* Variables pour la taille du texte */
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  /* Variables pour les animations */
24  --transition-speed: 0.3s;
25}
26
27/* Tryb ciemny */
28.theme-dark {
29  --background: #121212;
30  --foreground: #f3f4f6;
31  /* ... (autres couleurs pour le thème sombre) ... */
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. Hauteurs de composant pour différentes tailles de texte

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}

Implémentation pour les cas avancés

1. Mode sombre avec stockage côté serveur

Pour les applications avec authentification, les préférences utilisateur peuvent être stockées sur le serveur :

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 et pré-rendu avec prise en charge des préférences

1// proxy.ts (jusqu'à Next.js 15 : middleware.ts avec une fonction middleware)
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(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. Bascule automatique du mode sombre selon l'heure de la journée

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}

Bonnes pratiques

  1. Respecte les préférences système - vérifie toujours prefers-color-scheme comme réglage par défaut
  2. Évite les flashs - empêche le scintillement du contenu lors du changement de mode
  3. Des couleurs cohérentes - assure-toi que tous les éléments de l'interface réagissent correctement au changement de mode
  4. Teste minutieusement - vérifie l'apparence de l'application dans les deux modes
  5. L'accessibilité - souviens-toi que le mode sombre n'est pas qu'une question d'apparence, mais aussi d'accessibilité
  6. La performance - évite de réécrire de multiples classes et styles lors du changement
  7. Signale le mode actif - indique clairement quel mode est actuellement actif
  8. Sauvegarde les préférences - enregistre les préférences choisies par l'utilisateur entre les sessions

Implémentation avec TailwindCSS

Tailwind CSS s'intègre parfaitement avec le mode sombre :

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 ? 'Clair' : 'Sombre'}
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}

Résumé

Une implémentation efficace du mode sombre et la gestion des préférences utilisateur dans Next.js 16 sont un élément important de l'expérience utilisateur. Tout comme Quantum Metropolis adapte ses systèmes à l'heure de la journée et aux préférences de ses habitants, les applications web modernes devraient respecter les choix des utilisateurs tout en se souciant de l'accessibilité, de la performance et de la cohérence de l'interface.

Points clés à retenir :

  1. Respecte les préférences système - de nombreux utilisateurs ont déjà défini leur mode préféré dans le système
  2. Propose une bascule simple - permets de passer facilement d'un mode à l'autre
  3. Sauvegarde les choix de l'utilisateur - mémorise les préférences entre les sessions
  4. Conçois pour les deux modes - prends en compte les spécificités des deux modes dès l'étape de conception
  5. Fais attention à l'accessibilité - le mode sombre n'est pas qu'une question d'esthétique, mais aussi d'accessibilité
  6. Intègre-le aux autres préférences - permets aussi aux utilisateurs de personnaliser d'autres aspects de l'interface

Grâce à des outils modernes comme les variables CSS, l'API de contexte de React et des bibliothèques comme next-themes, l'implémentation du mode sombre et la gestion des préférences utilisateur deviennent plus simples et plus efficaces. Next.js 16 fournit une base solide pour créer des interfaces adaptatives qui répondent aux préférences des utilisateurs et offrent des expériences optimales quelles que soient les conditions.

Code de cette leçon : app/page.tsx
1// Accessibility & WCAG - Quantum Metropolis
2'use client';
3
4export default function AccessibilityDemo() {
5  return (
6    <div style={{
7      minHeight: '100vh',
8      background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
9      color: '#ffffff',
10      padding: '3rem 2rem'
11    }}>
12      <a href="#main-content" style={{
13        position: 'absolute',
14        left: '-9999px',
15        zIndex: 999,
16        padding: '1rem',
17        background: '#64ffda',
18        color: '#0f0f23',
19        textDecoration: 'none',
20        borderRadius: '0.5rem',
21        fontWeight: 600
22      }}>
23        Skip to main content
24      </a>
25
26      <header role="banner" style={{maxWidth: '1200px', margin: '0 auto 3rem'}}>
27        <h1 style={{
28          fontSize: '3rem',
29          textAlign: 'center',
30          background: 'linear-gradient(45deg, #64ffda, #7c4dff)',
31          WebkitBackgroundClip: 'text',
32          WebkitTextFillColor: 'transparent',
33          marginBottom: '1rem'
34        }}>
35          Accessibility & WCAG
36        </h1>
37        <p style={{textAlign: 'center', color: '#b0bec5', fontSize: '1.25rem'}}>
38          Building accessible interfaces for everyone
39        </p>
40      </header>
41
42      <main id="main-content" role="main" style={{maxWidth: '1200px', margin: '0 auto'}}>
43        {/* Semantic HTML */}
44        <section aria-labelledby="semantic-heading" style={{marginBottom: '3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
45          <h2 id="semantic-heading" style={{color: '#64ffda', marginBottom: '1.5rem'}}>1. Semantic HTML</h2>
46          <div style={{color: '#b0bec5', lineHeight: 1.8}}>
47            <p style={{marginBottom: '1rem'}}>Use semantic HTML tags instead of generic &lt;div&gt;:</p>
48            <ul style={{paddingLeft: '2rem', marginBottom: '1.5rem'}}>
49              <li>&lt;header&gt;, &lt;nav&gt;, &lt;main&gt;, &lt;article&gt;, &lt;section&gt;, &lt;aside&gt;, &lt;footer&gt;</li>
50              <li>&lt;button&gt; au lieu de &lt;div onclick&gt;</li>
51              <li>&lt;h1&gt; to &lt;h6&gt; for heading hierarchy</li>
52            </ul>
53            <pre style={{background: 'rgba(0,0,0,0.5)', padding: '1rem', borderRadius: '0.5rem', overflow: 'auto', fontSize: '0.875rem'}}>
54{`<article aria-labelledby="article-title">
55  <h2 id="article-title">Article title</h2>
56  <p>Article content...</p>
57</article>`}
58            </pre>
59          </div>
60        </section>
61
62        {/* ARIA Attributes */}
63        <section aria-labelledby="aria-heading" style={{marginBottom: '3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
64          <h2 id="aria-heading" style={{color: '#64ffda', marginBottom: '1.5rem'}}>2. ARIA Attributes</h2>
65
66          <div style={{marginBottom: '2rem'}}>
67            <h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Example: Button</h3>
68            <button
69              aria-label="Fermer la fenêtre modale"
70              style={{
71                padding: '0.75rem 2rem',
72                background: 'linear-gradient(135deg, #64ffda, #448aff)',
73                border: 'none',
74                borderRadius: '0.75rem',
75                color: '#0f0f23',
76                fontWeight: 600,
77                cursor: 'pointer',
78                transition: 'all 0.3s ease'
79              }}
80              onFocus={e => e.currentTarget.style.outline = '3px solid #64ffda'}
81              onBlur={e => e.currentTarget.style.outline = 'none'}
82            >
83              Close
84            </button>
85            <pre style={{background: 'rgba(0,0,0,0.5)', padding: '1rem', borderRadius: '0.5rem', overflow: 'auto', fontSize: '0.875rem', marginTop: '1rem'}}>
86{`<button aria-label="Fermer la fenêtre modale">
87  Close
88</button>`}
89            </pre>
90          </div>
91
92          <div>
93            <h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Example: Progress Bar</h3>
94            <div
95              role="progressbar"
96              aria-valuenow={75}
97              aria-valuemin={0}
98              aria-valuemax={100}
99              aria-label="Loading progress"
100              style={{width: '100%', height: '12px', background: 'rgba(255,255,255,0.1)', borderRadius: '6px', overflow: 'hidden'}}
101            >
102              <div style={{width: '75%', height: '100%', background: 'linear-gradient(90deg, #64ffda, #7c4dff)', borderRadius: '6px', transition: 'width 0.5s ease'}}></div>
103            </div>
104            <pre style={{background: 'rgba(0,0,0,0.5)', padding: '1rem', borderRadius: '0.5rem', overflow: 'auto', fontSize: '0.875rem', marginTop: '1rem'}}>
105{`<div
106  role="progressbar"
107  aria-valuenow="75"
108  aria-valuemin="0"
109  aria-valuemax="100"
110  aria-label="Loading progress"
111>
112  <div style="width: 75%"></div>
113</div>`}
114            </pre>
115          </div>
116        </section>
117
118        {/* Focus Management */}
119        <section aria-labelledby="focus-heading" style={{marginBottom: '3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
120          <h2 id="focus-heading" style={{color: '#64ffda', marginBottom: '1.5rem'}}>3. Focus Styles</h2>
121          <p style={{color: '#b0bec5', marginBottom: '1.5rem', lineHeight: 1.6}}>
122            Always provide visible focus states for keyboard-navigating users
123          </p>
124
125          <div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem'}}>
126            <button style={{
127              padding: '1rem',
128              background: 'rgba(100,255,218,0.2)',
129              border: '2px solid #64ffda',
130              borderRadius: '0.75rem',
131              color: '#64ffda',
132              fontWeight: 600,
133              cursor: 'pointer',
134              transition: 'all 0.3s ease'
135            }}
136            onFocus={e => {
137              e.currentTarget.style.outline = '3px solid #64ffda';
138              e.currentTarget.style.outlineOffset = '2px';
139            }}
140            onBlur={e => e.currentTarget.style.outline = 'none'}
141            >
142              Button 1
143            </button>
144
145            <button style={{
146              padding: '1rem',
147              background: 'rgba(124,77,255,0.2)',
148              border: '2px solid #7c4dff',
149              borderRadius: '0.75rem',
150              color: '#7c4dff',
151              fontWeight: 600,
152              cursor: 'pointer',
153              transition: 'all 0.3s ease'
154            }}
155            onFocus={e => {
156              e.currentTarget.style.outline = '3px solid #7c4dff';
157              e.currentTarget.style.outlineOffset = '2px';
158            }}
159            onBlur={e => e.currentTarget.style.outline = 'none'}
160            >
161              Button 2
162            </button>
163
164            <button style={{
165              padding: '1rem',
166              background: 'rgba(255,0,128,0.2)',
167              border: '2px solid #ff0080',
168              borderRadius: '0.75rem',
169              color: '#ff0080',
170              fontWeight: 600,
171              cursor: 'pointer',
172              transition: 'all 0.3s ease'
173            }}
174            onFocus={e => {
175              e.currentTarget.style.outline = '3px solid #ff0080';
176              e.currentTarget.style.outlineOffset = '2px';
177            }}
178            onBlur={e => e.currentTarget.style.outline = 'none'}
179            >
180              Button 3
181            </button>
182          </div>
183
184          <pre style={{background: 'rgba(0,0,0,0.5)', padding: '1rem', borderRadius: '0.5rem', overflow: 'auto', fontSize: '0.875rem', marginTop: '1.5rem'}}>
185{`button:focus {
186  outline: 3px solid var(--color-primary);
187  outline-offset: 2px;
188}`}
189          </pre>
190        </section>
191
192        {/* Color Contrast */}
193        <section aria-labelledby="contrast-heading" style={{marginBottom: '3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
194          <h2 id="contrast-heading" style={{color: '#64ffda', marginBottom: '1.5rem'}}>4. Color Contrast (WCAG AA)</h2>
195          <p style={{color: '#b0bec5', marginBottom: '1.5rem', lineHeight: 1.6}}>
196            Minimum contrast ratios according to WCAG:
197          </p>
198
199          <ul style={{color: '#b0bec5', lineHeight: 2, paddingLeft: '2rem', marginBottom: '1.5rem'}}>
200            <li>Normal text: 4.5:1</li>
201            <li>Large text (18pt+ ou 14pt+ bold): 3:1</li>
202            <li>UI components & graphics: 3:1</li>
203          </ul>
204
205          <div style={{display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1.5rem'}}>
206            <div style={{background: '#ffffff', padding: '1.5rem', borderRadius: '0.75rem'}}>
207              <p style={{color: '#212529', fontWeight: 600, marginBottom: '0.5rem'}}>Good Contrast</p>
208              <p style={{color: '#212529', fontSize: '0.875rem'}}>Black text on white background (21:1)</p>
209            </div>
210
211            <div style={{background: '#64ffda', padding: '1.5rem', borderRadius: '0.75rem'}}>
212              <p style={{color: '#0f0f23', fontWeight: 600, marginBottom: '0.5rem'}}>Good Contrast</p>
213              <p style={{color: '#0f0f23', fontSize: '0.875rem'}}>Dark text on cyan background (9.2:1)</p>
214            </div>
215          </div>
216        </section>
217
218        {/* Screen Reader Support */}
219        <section aria-labelledby="sr-heading" style={{marginBottom: '3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
220          <h2 id="sr-heading" style={{color: '#64ffda', marginBottom: '1.5rem'}}>5. Screen Reader Support</h2>
221
222          <div style={{marginBottom: '2rem'}}>
223            <h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Visually hidden but accessible to SR</h3>
224            <pre style={{background: 'rgba(0,0,0,0.5)', padding: '1rem', borderRadius: '0.5rem', overflow: 'auto', fontSize: '0.875rem'}}>
225{`.sr-only {
226  position: absolute;
227  width: 1px;
228  height: 1px;
229  padding: 0;
230  margin: -1px;
231  overflow: hidden;
232  clip: rect(0, 0, 0, 0);
233  white-space: nowrap;
234  border-width: 0;
235}`}
236            </pre>
237          </div>
238
239          <div>
240            <h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Live Regions</h3>
241            <pre style={{background: 'rgba(0,0,0,0.5)', padding: '1rem', borderRadius: '0.5rem', overflow: 'auto', fontSize: '0.875rem'}}>
242{`<div
243  aria-live="polite"
244  aria-atomic="true"
245  role="status"
246>
247  Data has been saved
248</div>`}
249            </pre>
250          </div>
251        </section>
252
253        {/* Best Practices Checklist */}
254        <section aria-labelledby="checklist-heading" style={{marginBottom: '3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
255          <h2 id="checklist-heading" style={{color: '#64ffda', marginBottom: '1.5rem'}}>Accessibility Checklist</h2>
256          <ul style={{color: '#b0bec5', lineHeight: 2.2, paddingLeft: '2rem', listStyle: 'none'}}>
257            <li>Use semantic HTML</li>
258            <li>Assure des labels ARIA appropriés</li>
259            <li>All interactive elements are keyboard accessible</li>
260            <li>Focus states are clearly visible</li>
261            <li>Color contrast meets WCAG AA (4.5:1)</li>
262            <li>Images have alt text</li>
263            <li>Forms have labels and error messages</li>
264            <li>Application works without JavaScript</li>
265            <li>Testé avec des lecteurs d'écran</li>
266            <li>Responsive and works on various screen sizes</li>
267          </ul>
268        </section>
269      </main>
270
271      <footer role="contentinfo" style={{maxWidth: '1200px', margin: '3rem auto 0', textAlign: 'center', padding: '2rem', borderTop: '1px solid rgba(100,255,218,0.2)', color: '#78909c'}}>
272        <p>© 2150 Quantum Metropolis • Accessibility First</p>
273      </footer>
274    </div>
275  );
276}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelle expression la leçon a-t-elle utilisée pour détecter la préférence de thème sombre du système ?

  2. 2. Où la leçon a-t-elle stocké le thème choisi par l’utilisateur pour qu’il survive à un rafraîchissement de page ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les étapes d’implémentation du mode sombre décrites dans la leçon :

Articles utiles