Cours JavaScript et React · Module 14: Styles avancés

Le theming dans Styled-components - Design System galactique

6 min de lecture
Dans cette leçon7

Imagine une flotte où chaque vaisseau a une nuance de violet différente sur ses boutons d'alarme, parce que chaque ingénieur a tapé la couleur de mémoire. Le jour où le commandement décide de changer les couleurs de la flotte, quelqu'un devra fouiller des centaines de fichiers et corriger chaque valeur #7c4dff à la main. Voilà à quoi ressemble un projet sans thème. Le ThemeProvider de styled-components permet de créer un système cohérent de couleurs, de tailles et de typographie - comme une interface unifiée pour toute la flotte spatiale.

Avant de commencer, une remarque honnête. En mars 2025, le mainteneur de la bibliothèque a annoncé que styled-components passait en mode maintenance : elle reçoit des corrections de bugs, mais plus de nouvelles fonctionnalités, et l'auteur lui-même ne la recommande pas pour les nouveaux projets. La bibliothèque (actuellement en version 6) fonctionne toujours dans un nombre énorme d'applications existantes, donc elle vaut la peine d'être connue, et l'idée de thème que tu vas apprendre ici se transpose telle quelle aux CSS Variables et à d'autres outils.

ThemeProvider

ThemeProvider est un composant qui fournit un thème (theme) à tous les composants qu'il contient. En interne, il utilise le contexte React, donc le thème atteint n'importe quelle profondeur sans passer de props. Le thème lui-même est un simple objet JavaScript dans lequel on regroupe les valeurs par catégorie : couleurs, espacements, arrondis et tailles de police :

1import { ThemeProvider } from 'styled-components';
2
3const spaceTheme = {
4  colors: {
5    primary: '#7c4dff',
6    secondary: '#64b5f6',
7    background: '#0a0a2e',
8    surface: '#1a1a4e',
9    text: '#e0e0e0',
10    success: '#00e676',
11    warning: '#ffa726',
12    danger: '#f44336',
13  },
14  spacing: {
15    xs: '4px',
16    sm: '8px',
17    md: '16px',
18    lg: '24px',
19    xl: '32px',
20  },
21  borderRadius: {
22    sm: '4px',
23    md: '8px',
24    lg: '12px',
25    round: '50%',
26  },
27  fontSize: {
28    sm: '12px',
29    md: '16px',
30    lg: '20px',
31    xl: '28px',
32  },
33};
34
35function App() {
36  return (
37    <ThemeProvider theme={spaceTheme}>
38      <Dashboard />
39    </ThemeProvider>
40  );
41}

ThemeProvider ne rend rien dans le DOM et ne change pas l'apparence de Dashboard à lui seul. Il se contente de fournir l'objet, et ce sont les composants qui doivent s'en servir. Les noms des clés, comme colors ou spacing, c'est toi qui les inventes, la bibliothèque n'impose aucune structure.

Utiliser le thème dans les composants

Chaque styled-component a automatiquement accès au thème via props.theme. À l'intérieur du template de style, tu insères une fonction dans ${...}, et la bibliothèque l'appelle avec les props du composant :

1const Panel = styled.div`
2  background: ${props => props.theme.colors.surface};
3  border-radius: ${props => props.theme.borderRadius.lg};
4  padding: ${props => props.theme.spacing.lg};
5  color: ${props => props.theme.colors.text};
6`;
7
8const Heading = styled.h2`
9  color: ${({ theme }) => theme.colors.primary};
10  font-size: ${({ theme }) => theme.fontSize.xl};
11  margin-bottom: ${({ theme }) => theme.spacing.md};
12`;

Remarque la déstructuration - ({ theme }) est un raccourci pour (props) => props.theme. Les deux écritures fonctionnent de façon identique, mais je te recommande la seconde, car avec beaucoup de valeurs elle est plus courte et plus lisible. Dans aucun de ces composants il ne reste une seule couleur codée en dur.

Changer de thème (sombre/clair)

L'une des utilisations les plus populaires de ThemeProvider est le passage du thème sombre au thème clair. On définit deux objets avec les mêmes clés, et l'état isDark choisit celui qui ira dans le provider :

1const darkTheme = {
2  colors: {
3    background: '#0a0a2e',
4    surface: '#1a1a4e',
5    text: '#e0e0e0',
6    primary: '#7c4dff',
7  },
8};
9
10const lightTheme = {
11  colors: {
12    background: '#f5f5f5',
13    surface: '#ffffff',
14    text: '#212121',
15    primary: '#5c6bc0',
16  },
17};
18
19function App() {
20  const [isDark, setIsDark] = useState(true);
21  const theme = isDark ? darkTheme : lightTheme;
22
23  return (
24    <ThemeProvider theme={theme}>
25      <AppContainer>
26        <button onClick={() => setIsDark(!isDark)}>
27          Toggle Theme
28        </button>
29        <Dashboard />
30      </AppContainer>
31    </ThemeProvider>
32  );
33}

Après un clic, seul l'objet passé à theme change, et tous les styled-components recalculent leurs styles. Le code des composants reste intact. Cela ne fonctionne que si les deux thèmes ont exactement les mêmes clés, car une clé manquante donne undefined dans le CSS.

Global Styles

createGlobalStyle permet de définir des styles CSS globaux qui ont eux aussi accès au thème. Il renvoie un composant que tu rends une seule fois, à l'intérieur de ThemeProvider :

1import { createGlobalStyle } from 'styled-components';
2
3const GlobalStyles = createGlobalStyle`
4  * {
5    margin: 0;
6    padding: 0;
7    box-sizing: border-box;
8  }
9
10  body {
11    background: ${({ theme }) => theme.colors.background};
12    color: ${({ theme }) => theme.colors.text};
13    font-family: 'Segoe UI', sans-serif;
14  }
15`;
16
17function App() {
18  return (
19    <ThemeProvider theme={spaceTheme}>
20      <GlobalStyles />
21      <Dashboard />
22    </ThemeProvider>
23  );
24}

GlobalStyles doit se trouver à l'intérieur du provider, sinon theme serait vide. Grâce à cela, le fond de toute la page change avec le thème, et pas seulement le fond de chaque panneau.

Le hook useTheme

Si tu as besoin d'accéder au thème dans la logique d'un composant (et pas seulement dans les styles), utilise le hook useTheme. Il renvoie le même objet que celui que tu as passé au provider :

1import { useTheme } from 'styled-components';
2
3function StatusChart({ data }) {
4  const theme = useTheme();
5
6  // Utiliser les couleurs du thème dans la logique, ex. pour Canvas, SVG ou des conditions
7  const chartColor = data.isHealthy
8    ? theme.colors.success
9    : theme.colors.danger;
10
11  return (
12    <div>
13      <svg width="100" height="100">
14        <circle cx="50" cy="50" r="40" fill={chartColor} />
15      </svg>
16      <p style={{ color: theme.colors.text }}>
17        Status: {data.isHealthy ? 'OK' : 'Alert'}
18      </p>
19    </div>
20  );
21}

useTheme est utile quand tu as besoin des valeurs du thème en dehors des styled-components - par exemple, pour dessiner sur un Canvas, configurer des bibliothèques de graphiques, ou pour une logique conditionnelle basée sur les couleurs du thème.

Thèmes imbriqués

ThemeProvider peut être imbriqué pour que différentes sections de l'application aient des thèmes différents. C'est le provider le plus proche qui l'emporte :

1function App() {
2  return (
3    <ThemeProvider theme={darkTheme}>
4      <Header />
5      <ThemeProvider theme={lightTheme}>
6        <ContentArea />  {/* lightTheme s'applique ici */}
7      </ThemeProvider>
8      <Footer />  {/* darkTheme est de retour ici */}
9    </ThemeProvider>
10  );
11}

C'est comme différentes sections d'un vaisseau spatial avec différents modes d'éclairage - la passerelle de commandement peut être sombre tandis que le laboratoire est lumineux. Le provider intérieur peut aussi recevoir une fonction theme={outer => ({ ...outer, ... })}, qui étend le thème extérieur au lieu de le remplacer.

Bonnes pratiques de theming

  1. Centralise les couleurs et les tailles - ne code jamais les valeurs en dur dans les composants, référence toujours le thème
  2. Utilise TypeScript - définis un type pour le thème afin d'obtenir l'autocomplétion (dans styled-components, tu étends l'interface DefaultTheme)
  3. Maintiens la cohérence - tous les thèmes doivent avoir les mêmes clés (colors, spacing, fontSize)
  4. Teste les deux thèmes - assure-toi que chaque composant a un bon rendu en mode sombre comme en mode clair

Dans la prochaine leçon, tu découvriras les patterns responsive, et dans le projet principal, tu combineras ce thème avec les CSS Modules et Tailwind dans un seul tableau de bord de mission.

Retiens : le thème, c'est la palette commune de la flotte, tu la modifies à un seul endroit, et chaque vaisseau brille aussitôt de ses nouvelles couleurs.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3
4// Deux thèmes -- sombre (cosmique) et clair
5const darkTheme = {
6  colors: {
7    primary: '#7c4dff',
8    secondary: '#64b5f6',
9    background: '#0a0a2e',
10    surface: '#1a1a4e',
11    text: '#e0e0e0',
12    textMuted: '#9e9e9e',
13    border: '#3949ab',
14    success: '#00e676',
15    warning: '#ffa726',
16    danger: '#f44336',
17  },
18  spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' },
19  borderRadius: { sm: '4px', md: '8px', lg: '12px' },
20  fontSize: { sm: '12px', md: '16px', lg: '20px', xl: '28px' },
21};
22
23const lightTheme = {
24  colors: {
25    primary: '#5c6bc0',
26    secondary: '#1976d2',
27    background: '#f5f5f5',
28    surface: '#ffffff',
29    text: '#212121',
30    textMuted: '#757575',
31    border: '#e0e0e0',
32    success: '#4caf50',
33    warning: '#ff9800',
34    danger: '#f44336',
35  },
36  spacing: darkTheme.spacing,
37  borderRadius: darkTheme.borderRadius,
38  fontSize: darkTheme.fontSize,
39};
40
41const GlobalStyles = createGlobalStyle`
42  * { margin: 0; padding: 0; box-sizing: border-box; }
43  body {
44    background: ${({ theme }) => theme.colors.background};
45    color: ${({ theme }) => theme.colors.text};
46    font-family: 'Segoe UI', sans-serif;
47    transition: background 0.3s ease, color 0.3s ease;
48  }
49`;
50
51const Container = styled.div`
52  max-width: 600px;
53  margin: 0 auto;
54  padding: ${({ theme }) => theme.spacing.lg};
55`;
56
57const Header = styled.header`
58  display: flex;
59  justify-content: space-between;
60  align-items: center;
61  margin-bottom: ${({ theme }) => theme.spacing.xl};
62`;
63
64const Title = styled.h1`
65  color: ${({ theme }) => theme.colors.primary};
66  font-size: ${({ theme }) => theme.fontSize.xl};
67`;
68
69const ThemeToggle = styled.button`
70  padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md};
71  background: ${({ theme }) => theme.colors.surface};
72  color: ${({ theme }) => theme.colors.text};
73  border: 2px solid ${({ theme }) => theme.colors.border};
74  border-radius: ${({ theme }) => theme.borderRadius.md};
75  cursor: pointer;
76  font-size: ${({ theme }) => theme.fontSize.md};
77  transition: all 0.2s ease;
78  &:hover { border-color: ${({ theme }) => theme.colors.primary}; }
79`;
80
81const Card = styled.div`
82  background: ${({ theme }) => theme.colors.surface};
83  border: 1px solid ${({ theme }) => theme.colors.border};
84  border-radius: ${({ theme }) => theme.borderRadius.lg};
85  padding: ${({ theme }) => theme.spacing.lg};
86  margin-bottom: ${({ theme }) => theme.spacing.md};
87  transition: all 0.3s ease;
88`;
89
90const CardTitle = styled.h3`
91  color: ${({ theme }) => theme.colors.primary};
92  margin-bottom: ${({ theme }) => theme.spacing.sm};
93`;
94
95const Badge = styled.span`
96  display: inline-block;
97  padding: 2px 10px;
98  border-radius: 12px;
99  font-size: ${({ theme }) => theme.fontSize.sm};
100  font-weight: bold;
101  background: ${({ theme, variant }) =>
102    variant === 'success' ? theme.colors.success + '33' :
103    variant === 'warning' ? theme.colors.warning + '33' :
104    theme.colors.danger + '33'};
105  color: ${({ theme, variant }) =>
106    variant === 'success' ? theme.colors.success :
107    variant === 'warning' ? theme.colors.warning :
108    theme.colors.danger};
109`;
110
111export default function App() {
112  const [isDark, setIsDark] = useState(true);
113
114  return (
115    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
116      <GlobalStyles />
117      <Container>
118        <Header>
119          <Title>Theme Demo</Title>
120          <ThemeToggle onClick={() => setIsDark(!isDark)}>
121            {isDark ? 'Light' : 'Dark'}
122          </ThemeToggle>
123        </Header>
124
125        <Card>
126          <CardTitle>Moteurs ioniques</CardTitle>
127          <p>Statut : <Badge variant="success">Actifs</Badge></p>
128          <p style={{ marginTop: '8px', fontSize: '14px' }}>
129            Pleine puissance -- prêts pour le saut hyperspatial.
130          </p>
131        </Card>
132
133        <Card>
134          <CardTitle>Boucliers énergétiques</CardTitle>
135          <p>Statut : <Badge variant="warning">Avertissement</Badge></p>
136          <p style={{ marginTop: '8px', fontSize: '14px' }}>
137            Niveau d'énergie : 42 %. Recharge nécessaire.
138          </p>
139        </Card>
140
141        <Card>
142          <CardTitle>Système de survie</CardTitle>
143          <p>Statut : <Badge variant="danger">Critique</Badge></p>
144          <p style={{ marginTop: '8px', fontSize: '14px' }}>
145            Réserves d'oxygène pour 72h. Intervention immédiate !
146          </p>
147        </Card>
148      </Container>
149    </ThemeProvider>
150  );
151}

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. Quel est le rôle du composant ThemeProvider dans styled-components ?

  2. 2. À quoi sert createGlobalStyle dans styled-components ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les éléments dans la bonne hiérarchie de ThemeProvider :

  • Éditeur de code

    Implémente le changement de thème avec ThemeProvider

  • Clique dans l'ordre

    Ordonne la syntaxe pour accéder à la couleur primary du thème dans un styled-component :

  • Éditeur de code

    Implémente une navigation avec un stylage dynamique du lien actif

Articles utiles