Skip to Content
React Native4. Styling & layout4.3 Thème & dark mode

Chapitre 4.3 — Thème & dark mode

Où on en est : vous savez styler et disposer. Mais nos couleurs sont éparpillées en dur (#58c4dc copié 12 fois). On centralise : palette, tokens, et dark mode propre.

⏱️ TL;DR — On définit la palette de Drill dans constants/theme.ts (tokens sémantiques : background, surface, text, accent…), on lit le mode système avec useColorScheme, et on distribue le thème par un hook maison useTheme. Drill assume un parti pris : sombre par défaut, clair disponible. Ce système sera remplacé au chapitre suivant par les variantes dark: de NativeWind — mais les tokens, eux, survivront.

🎯 Objectifs

  • Extraire une palette en tokens sémantiques (pas « bleu », mais « accent »).
  • Lire et suivre le thème système avec useColorScheme.
  • Construire un useTheme propre et l’utiliser dans les composants.
  • Comprendre pourquoi les tokens précèdent l’outil (StyleSheet aujourd’hui, NativeWind demain).

La palette de Drill

Un quiz se joue souvent le soir, dans le métro, au lit : sombre par défaut. La palette, inspirée du cyan React :

// constants/theme.ts export const palette = { // neutres ink950: '#0b1216', // fond app (sombre) ink900: '#101b21', // surfaces (cartes) ink700: '#1e3a45', // bordures ink400: '#7a8b94', // texte secondaire ink100: '#e2e8f0', // texte principal (sur sombre) white: '#ffffff', // marque cyan500: '#087ea4', cyan300: '#58c4dc', // sémantiques de jeu green500: '#22c55e', // bonne réponse red500: '#ef4444', // mauvaise réponse amber400: '#fbbf24', // streak / records } as const export type ThemeColors = { background: string surface: string border: string text: string textMuted: string accent: string success: string danger: string highlight: string } export const darkTheme: ThemeColors = { background: palette.ink950, surface: palette.ink900, border: palette.ink700, text: palette.ink100, textMuted: palette.ink400, accent: palette.cyan300, success: palette.green500, danger: palette.red500, highlight: palette.amber400, } export const lightTheme: ThemeColors = { background: '#f6fafc', surface: palette.white, border: '#d8e3e9', text: '#0f2530', textMuted: '#5b707b', accent: palette.cyan500, success: '#15803d', danger: '#b91c1c', highlight: '#b45309', }

La distinction clé : la palette nomme des couleurs (cyan300), le thème nomme des rôles (accent, surface). Les composants ne parlent QUE les rôles — c’est ce qui rend le dark/light trivial : même rôle, valeur différente.

💡 Pour un dev Next.js — C’est exactement vos variables CSS --background/--foreground

  • prefers-color-scheme, en objets TS. Même philosophie de design tokens ; seule la mécanique de distribution change (pas de CSS variables → un hook).

Lire le système : useColorScheme

import { useColorScheme } from 'react-native' const scheme = useColorScheme() // 'dark' | 'light' | null

Le hook est réactif : l’utilisateur bascule son téléphone en mode sombre → re-render. Condition : "userInterfaceStyle": "automatic" dans app.json (posé au chapitre 2.5 — sans ça, Android force une valeur fixe).

Le hook useTheme

// hooks/useTheme.ts import { useColorScheme } from 'react-native' import { darkTheme, lightTheme, ThemeColors } from '../constants/theme' export function useTheme(): ThemeColors { const scheme = useColorScheme() return scheme === 'light' ? lightTheme : darkTheme // sombre par défaut (null → dark) }

Usage dans un composant — les styles dépendant du thème deviennent dynamiques :

// components/ThemeCard.tsx — version thémée export function ThemeCard({ title, questionCount, emoji }: Props) { const t = useTheme() return ( <View style={[styles.card, { backgroundColor: t.surface, borderColor: t.border }]}> <Text style={styles.emoji}>{emoji}</Text> <View style={styles.body}> <Text style={[styles.title, { color: t.text }]} numberOfLines={1}>{title}</Text> <Text style={[styles.meta, { color: t.textMuted }]}>{questionCount} questions</Text> </View> </View> ) } const styles = StyleSheet.create({ // ce qui ne dépend PAS du thème reste statique card: { flexDirection: 'row', alignItems: 'center', gap: 12, borderRadius: 14, borderWidth: 1, padding: 16 }, emoji: { fontSize: 32 }, body: { flex: 1 }, title: { fontSize: 18, fontWeight: '700' }, meta: { fontSize: 13 }, })

Le pattern : structure statique dans StyleSheet, couleurs du thème en tableau. Verbeux ? Oui — c’est précisément ce que NativeWind va compresser au chapitre suivant (bg-surface dark:bg-… → une classe). Mais les tokens définis ici sont EXACTEMENT ceux qu’on branchera dans la config Tailwind : ce chapitre n’est pas jetable, c’est la fondation.

Et le choix manuel de l’utilisateur ?

Un vrai réglage de thème offre trois options : système / sombre / clair. Il faut donc UNE information persistée (la préférence) combinée au système. On l’implémentera proprement en Partie 6 (AsyncStorage + Zustand) — le useTheme ci-dessus prendra alors la préférence en compte sans que aucun composant ne change : ils parlent aux rôles, pas à la mécanique.

⚠️ Piège — Ne testez pas votre dark mode uniquement en dev : certaines couleurs « en dur » oubliées (un #fff de fond, le color d’un TextInput — chapitre 3.5 !) n’explosent que dans l’autre mode. Réflexe : basculer le téléphone light/dark fait partie de la checklist de CHAQUE écran. Et bannissez les hex littéraux dans les composants — si ce n’est pas un token, c’est un bug futur.

✏️ Exercices

1. Migrez AnswerButton (chapitre 3.3) vers les tokens : plus aucun hex littéral dedans.

✅ Solution

const t = useTheme() style={({ pressed }) => [ styles.base, { backgroundColor: t.surface, borderColor: t.border }, state === 'correct' && { borderColor: t.success, backgroundColor: t.success + '1f' }, state === 'incorrect' && { borderColor: t.danger, backgroundColor: t.danger + '1f' }, state === 'disabled' && styles.dimmed, pressed && interactive && styles.pressed, ]}

(+ '1f' ajoute ~12 % d’alpha à un hex 6 digits — l’astuce des fonds teintés. Le label passe en color: t.text.)

2. Ajoutez un rôle timerWarning (le timer sous 5 s) aux deux thèmes, sans casser le typage.

✅ Solution

Ajouter timerWarning: string au type ThemeColors, puis TS force à le définir dans darkTheme (palette.amber400) ET lightTheme ('#b45309'). C’est l’intérêt du type explicite : impossible d’oublier un mode. Usage : color: t.timerWarning quand secondsLeft <= 5.

3. Pourquoi préférer surface/accent à gray900/cyan300 dans les composants ? Donnez deux scénarios concrets où ça paie.

✅ Solution

(1) Dark/light : surface change de valeur selon le mode ; un composant écrit en gray900 serait faux en clair. (2) Rebranding/thèmes multiples : passer Drill en thème « ambre » = changer les thèmes, zéro composant touché. Les rôles découplent l’intention (« c’est une surface ») de la réalisation (« c’est #101b21 aujourd’hui »).

🧠 Quiz

1. Différence entre palette et thème ?

Réponse

La palette nomme des couleurs (cyan300) ; le thème mappe des rôles sémantiques (accent, surface, text) vers ces couleurs — un mapping par mode (dark/light). Les composants ne consomment que les rôles.

2. Que retourne useColorScheme et à quelle condition suit-il le système ?

Réponse

'dark' | 'light' | null, réactif aux changements système — à condition que userInterfaceStyle: "automatic" soit dans app.json.

3. Quelle partie d’un style reste dans StyleSheet.create avec ce système ?

Réponse

La structure (layout, tailles, radius, typo) — tout ce qui ne dépend pas du mode. Les couleurs du thème s’ajoutent en tableau : [styles.card, { backgroundColor: t.surface }].

4. Pourquoi Drill est-il sombre par défaut (null → dark) ?

Réponse

Parti pris produit : usage du soir/mobilité, confort visuel d’un quiz, identité visuelle. Le null (système indéterminé) retombe sur le mode principal du design.

5. Où sera branché le choix manuel système/sombre/clair, et pourquoi pas maintenant ?

Réponse

En Partie 6 : il faut persister la préférence (AsyncStorage) et la distribuer (Zustand). Grâce aux rôles, ce branchement ne modifiera aucun composant — seulement le hook useTheme.


👉 Chapitre suivant : 4.4 — NativeWind — le retour de Tailwind. Vos classes, vos réflexes, dans React Native.