Cours JavaScript et React · Module 7: Styles et animations

Styled Components - Fusion cosmique de JSX et de CSS

25 min de lecture
Dans cette leçon10

Lors de notre voyage cosmique à travers le monde du style dans React, nous arrivons à une technologie fascinante qui efface les frontières entre code et style. Styled Components est comme un vaisseau interstellaire avancé qui combine l'ADN de deux mondes - JavaScript et CSS - créant une nouvelle forme de vie parfaitement adaptée à l'écosystème React.

Que sont les Styled Components ?

Styled Components est l'une des bibliothèques CSS-in-JS les plus populaires : elle permet d'écrire du vrai CSS directement dans les fichiers JavaScript/JSX, tout en créant des composants React normaux. Cette approche révolutionnaire du style nous permet de penser nos composants comme des unités totalement autonomes et fermées, où la logique et le style sont rassemblés au même endroit.

Imagine les composants comme des modules de vaisseau : chacun contient à la fois sa fonction (moteurs, systèmes de survie, navigation) et son apparence (panneaux de contrôle, indicateurs, interfaces) - le tout hermétiquement scellé dans une unité cohérente.

Installation et configuration

Avant de commencer notre aventure avec Styled Components, nous devons installer la bibliothèque :

1npm install styled-components
2# ou
3yarn add styled-components
4# ou
5pnpm add styled-components

Pour les utilisateurs de TypeScript, il vaut la peine d'ajouter aussi les types :

1npm install @types/styled-components

Les bases de Styled Components

Créer un composant stylisé simple

Créer des styled components est extrêmement intuitif. Il suffit d'utiliser la fonction styled importée depuis la bibliothèque, suivie du nom de la balise HTML à laquelle tu veux appliquer des styles :

1import styled from 'styled-components';
2
3// Création d'un bouton stylisé
4const Button = styled.button`
5  background-color: #4a90e2;
6  color: white;
7  padding: 10px 15px;
8  border: none;
9  border-radius: 4px;
10  font-weight: bold;
11  cursor: pointer;
12  transition: all 0.3s ease;
13
14  &:hover {
15    background-color: #357ae8;
16    transform: translateY(-2px);
17    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
18  }
19
20  &:active {
21    transform: translateY(1px);
22  }
23`;
24
25// Utilisation du composant
26function App() {
27  return (
28    <div>
29      <h1>Command Center</h1>
30      <Button>Initiate launch procedure</Button>
31    </div>
32  );
33}

Dans l'exemple ci-dessus, nous avons créé un composant stylisé Button qui fonctionne comme un bouton HTML normal mais avec des styles prédéfinis. Remarque la syntaxe caractéristique utilisant les backticks (``), qui permet d'écrire du CSS multi-lignes. De plus, nous pouvons utiliser le sélecteur & comme en Sass pour faire référence au composant lui-même.

Héritage de styles

Nous pouvons étendre des styled components existants en ajoutant de nouveaux styles :

1// Bouton de base
2const Button = styled.button`
3  padding: 10px 15px;
4  border: none;
5  border-radius: 4px;
6  font-weight: bold;
7  cursor: pointer;
8  transition: all 0.3s ease;
9`;
10
11// Extension du bouton de base
12const PrimaryButton = styled(Button)`
13  background-color: #4a90e2;
14  color: white;
15
16  &:hover {
17    background-color: #357ae8;
18  }
19`;
20
21// Une autre extension
22const DangerButton = styled(Button)`
23  background-color: #e74c3c;
24  color: white;
25
26  &:hover {
27    background-color: #c0392b;
28  }
29`;

Travailler avec les props (paramètres)

L'une des fonctionnalités les plus puissantes de Styled Components est la possibilité de styliser dynamiquement en fonction des props passées au composant :

1const Button = styled.button`
2  padding: 10px 15px;
3  border: none;
4  border-radius: 4px;
5  font-weight: bold;
6  cursor: pointer;
7  transition: all 0.3s ease;
8
9  /* Couleurs dynamiques en fonction des props */
10  background-color: ${props =>
11    props.primary ? '#4a90e2' : props.danger ? '#e74c3c' : props.success ? '#2ecc71' : '#95a5a6'};
12  color: white;
13
14  /* Taille dynamique */
15  font-size: ${props => (props.large ? '18px' : '14px')};
16  padding: ${props => (props.large ? '12px 20px' : '8px 12px')};
17
18  /* Bouton désactivé */
19  opacity: ${props => (props.disabled ? 0.5 : 1)};
20  cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
21
22  &:hover {
23    ${props => !props.disabled && `
24      transform: translateY(-2px);
25      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
26    `}
27  }
28`;
29
30// Utilisation avec différentes props
31function ActionPanel() {
32  return (
33    <div>
34      <Button>Normal</Button>
35      <Button primary>Primary</Button>
36      <Button danger>Delete</Button>
37      <Button success large>Save</Button>
38      <Button disabled>Unavailable</Button>
39    </div>
40  );
41}

Dans l'exemple ci-dessus, le style du bouton change selon les props passées. Nous pouvons passer n'importe quelles props à nos styled components et les utiliser ensuite dans des expressions à l'intérieur des template literals.

Transmission des props et attributs HTML

Styled Components transmet automatiquement toutes les props inutilisées à l'élément DOM :

1// Ce bouton aura le type "submit" et la gestion du onClick
2function FormSubmit() {
3  return (
4    <Button
5      type="submit"
6      onClick={() => console.log('Form submitted!')}
7    >
8      Submit
9    </Button>
10  );
11}

Styliser des composants existants

Nous pouvons aussi styliser des composants React existants, à condition qu'ils acceptent une prop className :

1// Composant React classique
2function CustomInput({ className, ...props }) {
3  return (
4    <input
5      className={className}
6      {...props}
7    />
8  );
9}
10
11// Stylisation d'un composant existant
12const StyledCustomInput = styled(CustomInput)`
13  border: 2px solid #3498db;
14  border-radius: 4px;
15  padding: 8px 12px;
16  width: 100%;
17  font-size: 16px;
18
19  &:focus {
20    outline: none;
21    border-color: #2980b9;
22    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
23  }
24`;

Fonctionnalités avancées de Styled Components

Styles globaux

Styled Components propose aussi un composant GlobalStyle pour définir des styles globaux :

1import { createGlobalStyle } from 'styled-components';
2
3const GlobalStyle = createGlobalStyle`
4  /* CSS Reset */
5  *, *::before, *::after {
6    box-sizing: border-box;
7    margin: 0;
8    padding: 0;
9  }
10
11  /* Variables globales */
12  :root {
13    --primary-color: #4a90e2;
14    --secondary-color: #8a2be2;
15    --success-color: #2ecc71;
16    --danger-color: #e74c3c;
17    --warning-color: #f1c40f;
18    --text-light: #ecf0f1;
19    --text-dark: #2c3e50;
20    --background-dark: #1a1a2e;
21    --background-light: #f5f5f5;
22  }
23
24  body {
25    font-family: 'Space Grotesk', sans-serif;
26    line-height: 1.6;
27    background-color: var(--background-dark);
28    color: var(--text-light);
29  }
30
31  a {
32    color: var(--primary-color);
33    text-decoration: none;
34  }
35
36  button {
37    font-family: inherit;
38  }
39`;
40
41// Utilisation de GlobalStyle dans l'application
42function App() {
43  return (
44    <>
45      <GlobalStyle />
46      <div>
47        {/* Contenu de l'application */}
48      </div>
49    </>
50  );
51}

Theming (thèmes)

L'un des plus grands avantages de Styled Components est la prise en charge des thèmes :

1import { ThemeProvider } from 'styled-components';
2
3// Définition du thème
4const darkTheme = {
5  colors: {
6    primary: '#4a90e2',
7    secondary: '#8a2be2',
8    success: '#2ecc71',
9    danger: '#e74c3c',
10    warning: '#f1c40f',
11    background: '#1a1a2e',
12    text: '#ecf0f1',
13    muted: '#95a5a6'
14  },
15  fonts: {
16    body: "'Space Grotesk', sans-serif",
17    heading: "'Nova Mono', monospace"
18  },
19  spacing: {
20    xs: '4px',
21    sm: '8px',
22    md: '16px',
23    lg: '24px',
24    xl: '32px'
25  },
26  borderRadius: {
27    small: '4px',
28    medium: '8px',
29    large: '12px',
30    round: '50%'
31  },
32  shadows: {
33    small: '0 2px 4px rgba(0, 0, 0, 0.1)',
34    medium: '0 4px 8px rgba(0, 0, 0, 0.2)',
35    large: '0 8px 16px rgba(0, 0, 0, 0.3)'
36  }
37};
38
39// Thème alternatif
40const lightTheme = {
41  colors: {
42    primary: '#3498db',
43    secondary: '#9b59b6',
44    success: '#27ae60',
45    danger: '#c0392b',
46    warning: '#f39c12',
47    background: '#f5f5f5',
48    text: '#2c3e50',
49    muted: '#7f8c8d'
50  },
51  // Autres propriétés identiques à darkTheme
52  fonts: darkTheme.fonts,
53  spacing: darkTheme.spacing,
54  borderRadius: darkTheme.borderRadius,
55  shadows: darkTheme.shadows
56};
57
58// Styled component utilisant le thème
59const Card = styled.div`
60  background-color: ${props =>
61    props.theme.colors.background === '#1a1a2e' ? '#252941' : '#ffffff'};
62  color: ${props => props.theme.colors.text};
63  border-radius: ${props => props.theme.borderRadius.medium};
64  padding: ${props => props.theme.spacing.md};
65  box-shadow: ${props => props.theme.shadows.small};
66
67  transition: all 0.3s ease;
68
69  &:hover {
70    box-shadow: ${props => props.theme.shadows.medium};
71  }
72`;
73
74// Bouton stylisé utilisant le thème
75const Button = styled.button`
76  background-color: ${props => props.theme.colors.primary};
77  color: white;
78  padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};
79  border: none;
80  border-radius: ${props => props.theme.borderRadius.small};
81  font-family: ${props => props.theme.fonts.body};
82  cursor: pointer;
83
84  &:hover {
85    background-color: ${props => {
86      const color = props.theme.colors.primary;
87      // Fonction simple pour assombrir la couleur
88      return color.replace(/rgb\((\d+), (\d+), (\d+)\)/, (_, r, g, b) =>
89        `rgb(${Math.max(0, r - 20)}, ${Math.max(0, g - 20)}, ${Math.max(0, b - 20)})`
90      );
91    }};
92  }
93`;
94
95// Utilisation de ThemeProvider et des composants à thème
96function App() {
97  const [darkMode, setDarkMode] = useState(true);
98  const theme = darkMode ? darkTheme : lightTheme;
99
100  return (
101    <ThemeProvider theme={theme}>
102      <GlobalStyle />
103      <AppContainer>
104        <Header>
105          <h1>Cosmic Command Center</h1>
106          <Button onClick={() => setDarkMode(!darkMode)}>
107            Passer en mode {darkMode ? "clair" : "sombre"}
108          </Button>
109        </Header>
110
111        <Card>
112          <h2>Panneau de contrôle</h2>
113          <p>Ici tu peux gérer ta mission cosmique.</p>
114          <Button>Lancer</Button>
115        </Card>
116      </AppContainer>
117    </ThemeProvider>
118  );
119}

Animations

Styled Components permet de créer facilement des animations grâce à la fonction utilitaire keyframes :

1import styled, { keyframes } from 'styled-components';
2
3// Définition de l'animation
4const pulse = keyframes`
5  0% {
6    opacity: 1;
7    transform: scale(1);
8  }
9  50% {
10    opacity: 0.8;
11    transform: scale(1.05);
12  }
13  100% {
14    opacity: 1;
15    transform: scale(1);
16  }
17`;
18
19const rotate = keyframes`
20  from {
21    transform: rotate(0deg);
22  }
23  to {
24    transform: rotate(360deg);
25  }
26`;
27
28// Utilisation des animations dans un composant
29const PulsingButton = styled.button`
30  background-color: #e74c3c;
31  color: white;
32  padding: 12px 24px;
33  border: none;
34  border-radius: 4px;
35  font-weight: bold;
36
37  /* Application de l'animation */
38  animation: ${pulse} 2s infinite ease-in-out;
39`;
40
41const Spinner = styled.div`
42  width: 40px;
43  height: 40px;
44  border: 4px solid rgba(255, 255, 255, 0.3);
45  border-top: 4px solid #3498db;
46  border-radius: 50%;
47
48  /* Animation de rotation */
49  animation: ${rotate} 1s linear infinite;
50`;
51
52// Animations conditionnelles
53const AlertBadge = styled.span`
54  display: inline-block;
55  padding: 4px 8px;
56  background-color: red;
57  color: white;
58  border-radius: 10px;
59  font-size: 12px;
60
61  /* Animation uniquement en cas d'alerte critique */
62  animation: ${props => props.critical ?
63    css`${pulse} 1s infinite` : 'none'};
64`;

Pseudo-éléments et pseudo-classes

Styled Components prend en charge tous les pseudo-éléments et pseudo-classes CSS :

1const HoverCard = styled.div`
2  position: relative;
3  padding: 20px;
4  background-color: #252941;
5  border-radius: 8px;
6  transition: all 0.3s ease;
7
8  /* Pseudo-classes */
9  &:hover {
10    transform: translateY(-5px);
11    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
12  }
13
14  &:active {
15    transform: translateY(-2px);
16  }
17
18  /* Pseudo-éléments */
19  &::before {
20    content: "";
21    position: absolute;
22    top: -10px;
23    left: -10px;
24    right: -10px;
25    bottom: -10px;
26    border-radius: 12px;
27    background: linear-gradient(45deg, #4a90e2, #8a2be2);
28    z-index: -1;
29    opacity: 0;
30    transition: opacity 0.3s ease;
31  }
32
33  &:hover::before {
34    opacity: 1;
35  }
36
37  /* Sélecteurs imbriqués */
38  h3 {
39    margin-bottom: 10px;
40    color: #ecf0f1;
41  }
42
43  p {
44    color: #bdc3c7;
45  }
46
47  /* Sélecteurs complexes */
48  &:hover h3 {
49    color: #4a90e2;
50  }
51`;

Media queries

Styled Components prend très bien en charge les media queries, permettant de créer des composants responsives :

1const ResponsiveContainer = styled.div`
2  padding: 20px;
3  background-color: #252941;
4  border-radius: 8px;
5
6  /* Largeur de base */
7  width: 100%;
8
9  /* Media queries */
10  @media (min-width: 768px) {
11    width: 750px;
12    margin: 0 auto;
13  }
14
15  @media (min-width: 992px) {
16    width: 970px;
17  }
18
19  @media (min-width: 1200px) {
20    width: 1170px;
21  }
22
23  /* Changements de style responsives */
24  h1 {
25    font-size: 24px;
26
27    @media (min-width: 768px) {
28      font-size: 32px;
29    }
30  }
31
32  /* On peut utiliser des media queries à l'intérieur de conditions */
33  display: ${props => props.hidden ?
34    `
35      none;
36      @media (min-width: 768px) {
37        display: block;
38      }
39    ` : 'block'
40  };
41`;

Helpers et fonctions utilitaires

Styled Components fournit plusieurs outils utiles, comme css pour réutiliser des fragments de style :

1import styled, { css } from 'styled-components';
2
3// Création de fragments de style réutilisables
4const flexCenter = css`
5  display: flex;
6  justify-content: center;
7  align-items: center;
8`;
9
10const buttonBase = css`
11  padding: 10px 15px;
12  border: none;
13  border-radius: 4px;
14  font-weight: bold;
15  cursor: pointer;
16  transition: all 0.3s ease;
17`;
18
19const dangerStyles = css`
20  background-color: #e74c3c;
21  color: white;
22
23  &:hover {
24    background-color: #c0392b;
25  }
26`;
27
28// Utilisation des fragments dans les composants
29const FlexContainer = styled.div`
30  ${flexCenter}
31  gap: 20px;
32  flex-wrap: wrap;
33`;
34
35const Button = styled.button`
36  ${buttonBase}
37
38  background-color: #4a90e2;
39  color: white;
40
41  &:hover {
42    background-color: #357ae8;
43  }
44
45  ${props => props.danger && dangerStyles}
46`;

Patterns et techniques avancés

Composants réutilisables et leurs variantes

L'une des meilleures pratiques avec Styled Components est de créer des composants de base réutilisables avec des variantes :

1// Composant bouton de base
2const BaseButton = styled.button`
3  padding: 10px 15px;
4  border: none;
5  border-radius: 4px;
6  font-weight: bold;
7  cursor: pointer;
8  transition: all 0.3s ease;
9  font-size: 14px;
10`;
11
12// Variantes de bouton
13const PrimaryButton = styled(BaseButton)`
14  background-color: ${props => props.theme.colors.primary};
15  color: white;
16
17  &:hover {
18    background-color: ${props => darken(0.1, props.theme.colors.primary)};
19  }
20`;
21
22const SecondaryButton = styled(BaseButton)`
23  background-color: transparent;
24  color: ${props => props.theme.colors.primary};
25  border: 2px solid ${props => props.theme.colors.primary};
26
27  &:hover {
28    background-color: ${props => rgba(props.theme.colors.primary, 0.1)};
29  }
30`;
31
32const DangerButton = styled(BaseButton)`
33  background-color: ${props => props.theme.colors.danger};
34  color: white;
35
36  &:hover {
37    background-color: ${props => darken(0.1, props.theme.colors.danger)};
38  }
39`;
40
41// Tailles de bouton
42const SmallButton = css`
43  padding: 6px 10px;
44  font-size: 12px;
45`;
46
47const LargeButton = css`
48  padding: 12px 20px;
49  font-size: 16px;
50`;
51
52// Composant d'ordre supérieur pour la taille
53const sizedButton = (Component) => styled(Component)`
54  ${props => props.small && SmallButton}
55  ${props => props.large && LargeButton}
56`;
57
58// Création de variantes avec tailles
59const SizedPrimaryButton = sizedButton(PrimaryButton);
60const SizedSecondaryButton = sizedButton(SecondaryButton);
61const SizedDangerButton = sizedButton(DangerButton);
62
63// Utilisation
64function ActionPanel() {
65  return (
66    <div>
67      <SizedPrimaryButton>Normal</SizedPrimaryButton>
68      <SizedPrimaryButton small>Petit</SizedPrimaryButton>
69      <SizedPrimaryButton large>Grand</SizedPrimaryButton>
70
71      <SizedSecondaryButton>Normal</SizedSecondaryButton>
72      <SizedDangerButton large>Grosse alerte</SizedDangerButton>
73    </div>
74  );
75}

Usage créatif de l'attribut as

Styled Components permet de changer l'élément HTML utilisé par un composant via la prop as :

1const Button = styled.button`
2  background-color: #4a90e2;
3  color: white;
4  padding: 10px 15px;
5  border: none;
6  border-radius: 4px;
7  font-weight: bold;
8  cursor: pointer;
9  display: inline-flex;
10  align-items: center;
11  justify-content: center;
12  text-decoration: none;
13`;
14
15function App() {
16  return (
17    <div>
18      <Button>Bouton normal</Button>
19
20      {/* Rendu sous forme de lien */}
21      <Button as="a" href="/destination">
22        Bouton sous forme de lien
23      </Button>
24
25      {/* Rendu sous forme d'un autre élément */}
26      <Button as="div" role="button" tabIndex={0}>
27        Bouton sous forme de div
28      </Button>
29
30      {/* On peut utiliser un autre composant React */}
31      <Button as={Link} to="/dashboard">
32        Bouton sous forme de Link de router
33      </Button>
34    </div>
35  );
36}

Stylisation conditionnelle

Styled Components permet une stylisation conditionnelle très détaillée :

1const AlertBox = styled.div`
2  padding: 15px;
3  border-radius: 8px;
4  margin-bottom: 20px;
5  display: flex;
6  align-items: center;
7
8  /* Changement de couleur de fond selon le type d'alerte */
9  background-color: ${props =>
10    props.type === 'success' ? '#d4edda' :
11    props.type === 'danger' ? '#f8d7da' :
12    props.type === 'warning' ? '#fff3cd' :
13    props.type === 'info' ? '#d1ecf1' :
14    '#e2e3e5'
15  };
16
17  /* Changement de couleur du texte */
18  color: ${props =>
19    props.type === 'success' ? '#155724' :
20    props.type === 'danger' ? '#721c24' :
21    props.type === 'warning' ? '#856404' :
22    props.type === 'info' ? '#0c5460' :
23    '#383d41'
24  };
25
26  /* Changement de bordure */
27  border: 1px solid ${props =>
28    props.type === 'success' ? '#c3e6cb' :
29    props.type === 'danger' ? '#f5c6cb' :
30    props.type === 'warning' ? '#ffeeba' :
31    props.type === 'info' ? '#bee5eb' :
32    '#d6d8db'
33  };
34
35  /* Changement des icônes */
36  &::before {
37    content: "${props =>
38      props.type === 'success' ? 'OK' :
39      props.type === 'danger' ? '×' :
40      props.type === 'warning' ? '!' :
41      props.type === 'info' ? 'ℹ' :
42      'ℹ'
43    }";
44    margin-right: 10px;
45    font-size: 18px;
46  }
47
48  /* On peut aussi utiliser des conditions et fonctions imbriquées */
49  ${props => {
50    if (props.dismissable) {
51      return `
52        padding-right: 40px;
53        position: relative;
54      `;
55    }
56  }}
57`;
58
59// Bouton de fermeture pour les alertes qu'on peut masquer
60const CloseButton = styled.button`
61  position: absolute;
62  top: 10px;
63  right: 10px;
64  background: none;
65  border: none;
66  font-size: 20px;
67  cursor: pointer;
68  color: inherit;
69  opacity: 0.5;
70
71  &:hover {
72    opacity: 1;
73  }
74`;
75
76// Utilisation du composant
77function Notifications() {
78  return (
79    <div>
80      <AlertBox type="success">
81        Mission accomplie avec succès !
82      </AlertBox>
83
84      <AlertBox type="danger" dismissable>
85        Attention : panne système détectée !
86        <CloseButton>&times;</CloseButton>
87      </AlertBox>
88
89      <AlertBox type="warning">
90        Attention : niveau de carburant faible.
91      </AlertBox>
92
93      <AlertBox type="info">
94        Information : approche de la station spatiale.
95      </AlertBox>
96    </div>
97  );
98}

Exemple pratique : Tableau de bord cosmique

Voyons comment créer une interface plus complexe à l'aide de Styled Components :

1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle, keyframes } from 'styled-components';
3
4// ========== Définition du thème ==========
5const theme = {
6  colors: {
7    primary: '#4a90e2',
8    secondary: '#8a2be2',
9    success: '#2ecc71',
10    danger: '#e74c3c',
11    warning: '#f1c40f',
12    info: '#3498db',
13    background: '#0f1429',
14    cardBg: '#1a1f3c',
15    text: '#ecf0f1',
16    textMuted: '#95a5a6',
17    border: '#2c3e50'
18  },
19  spacing: {
20    xs: '4px',
21    sm: '8px',
22    md: '16px',
23    lg: '24px',
24    xl: '32px'
25  },
26  fonts: {
27    body: "'Space Grotesk', sans-serif",
28    mono: "'JetBrains Mono', monospace"
29  },
30  borderRadius: {
31    sm: '4px',
32    md: '8px',
33    lg: '16px'
34  }
35};
36
37// ========== Style global ==========
38const GlobalStyle = createGlobalStyle`
39  * {
40    box-sizing: border-box;
41    margin: 0;
42    padding: 0;
43  }
44
45  body {
46    font-family: ${props => props.theme.fonts.body};
47    background-color: ${props => props.theme.colors.background};
48    color: ${props => props.theme.colors.text};
49    line-height: 1.6;
50  }
51
52  h1, h2, h3, h4, h5, h6 {
53    margin-bottom: ${props => props.theme.spacing.md};
54  }
55`;
56
57// ========== Animations ==========
58const fadeIn = keyframes`
59  from {
60    opacity: 0;
61    transform: translateY(10px);
62  }
63  to {
64    opacity: 1;
65    transform: translateY(0);
66  }
67`;
68
69const pulse = keyframes`
70  0% {
71    transform: scale(1);
72  }
73  50% {
74    transform: scale(1.05);
75  }
76  100% {
77    transform: scale(1);
78  }
79`;
80
81const rotate = keyframes`
82  from {
83    transform: rotate(0deg);
84  }
85  to {
86    transform: rotate(360deg);
87  }
88`;
89
90// ========== Composants de mise en page ==========
91const AppContainer = styled.div`
92  max-width: 1200px;
93  margin: 0 auto;
94  padding: ${props => props.theme.spacing.lg};
95`;
96
97const Header = styled.header`
98  display: flex;
99  justify-content: space-between;
100  align-items: center;
101  margin-bottom: ${props => props.theme.spacing.xl};
102  padding-bottom: ${props => props.theme.spacing.md};
103  border-bottom: 1px solid ${props => props.theme.colors.border};
104
105  animation: ${fadeIn} 0.5s ease-out forwards;
106`;
107
108const MainTitle = styled.h1`
109  color: ${props => props.theme.colors.primary};
110  font-size: 28px;
111  display: flex;
112  align-items: center;
113
114  &::before {
115    content: "▲";
116    margin-right: ${props => props.theme.spacing.sm};
117  }
118`;
119
120const Grid = styled.div`
121  display: grid;
122  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
123  gap: ${props => props.theme.spacing.lg};
124  margin-bottom: ${props => props.theme.spacing.xl};
125
126  animation: ${fadeIn} 0.5s ease-out forwards;
127  animation-delay: 0.2s;
128  opacity: 0;
129`;
130
131const WideSection = styled.section`
132  grid-column: 1 / -1;
133  animation: ${fadeIn} 0.5s ease-out forwards;
134  animation-delay: 0.4s;
135  opacity: 0;
136`;
137
138// ========== Composants de carte ==========
139const Card = styled.div`
140  background-color: ${props => props.theme.colors.cardBg};
141  border-radius: ${props => props.theme.borderRadius.md};
142  padding: ${props => props.theme.spacing.lg};
143  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
144
145  ${props => props.interactive && `
146    cursor: pointer;
147    transition: all 0.3s ease;
148
149    &:hover {
150      transform: translateY(-5px);
151      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
152    }
153  `}
154
155  ${props => props.primary && `
156    border-top: 4px solid ${props.theme.colors.primary};
157  `}
158
159  ${props => props.warning && `
160    border-top: 4px solid ${props.theme.colors.warning};
161  `}
162
163  ${props => props.danger && `
164    border-top: 4px solid ${props.theme.colors.danger};
165  `}
166
167  ${props => props.success && `
168    border-top: 4px solid ${props.theme.colors.success};
169  `}
170`;
171
172const CardTitle = styled.h2`
173  font-size: 18px;
174  display: flex;
175  align-items: center;
176  margin-bottom: ${props => props.theme.spacing.md};
177
178  ${props => props.icon && `
179    &::before {
180      content: "${props.icon}";
181      margin-right: ${props.theme.spacing.sm};
182    }
183  `}
184`;
185
186const CardContent = styled.div`
187  margin-bottom: ${props => props.theme.spacing.md};
188`;
189
190// ========== Composants de bouton ==========
191const Button = styled.button`
192  background-color: ${props =>
193    props.primary ? props.theme.colors.primary :
194    props.danger ? props.theme.colors.danger :
195    props.success ? props.theme.colors.success :
196    props.warning ? props.theme.colors.warning :
197    '#95a5a6'
198  };
199  color: white;
200  border: none;
201  border-radius: ${props => props.theme.borderRadius.sm};
202  padding: ${props => props.small ? '6px 12px' : '10px 20px'};
203  font-family: ${props => props.theme.fonts.body};
204  font-size: ${props => props.small ? '12px' : '14px'};
205  font-weight: bold;
206  cursor: pointer;
207  transition: all 0.3s ease;
208
209  &:hover {
210    filter: brightness(1.1);
211    transform: translateY(-2px);
212    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
213  }
214
215  &:active {
216    transform: translateY(0);
217  }
218
219  ${props => props.ghost && `
220    background-color: transparent;
221    color: ${
222      props.primary ? props.theme.colors.primary :
223      props.danger ? props.theme.colors.danger :
224      props.success ? props.theme.colors.success :
225      props.warning ? props.theme.colors.warning :
226      props.theme.colors.text
227    };
228    border: 1px solid currentColor;
229
230    &:hover {
231      background-color: rgba(255, 255, 255, 0.05);
232    }
233  `}
234
235  ${props => props.icon && `
236    display: inline-flex;
237    align-items: center;
238
239    &::before {
240      content: "${props.icon}";
241      margin-right: 8px;
242    }
243  `}
244
245  ${props => props.block && `
246    display: block;
247    width: 100%;
248  `}
249
250  ${props => props.disabled && `
251    opacity: 0.5;
252    cursor: not-allowed;
253    pointer-events: none;
254  `}
255
256  & + & {
257    margin-left: ${props => props.theme.spacing.sm};
258  }
259`;
260
261// ========== Composants d'affichage de données ==========
262const Statistic = styled.div`
263  text-align: center;
264`;
265
266const StatValue = styled.div`
267  font-size: 36px;
268  font-weight: bold;
269  color: ${props =>
270    props.primary
271      ? props.theme.colors.primary
272      : props.danger
273      ? props.theme.colors.danger
274      : props.success
275      ? props.theme.colors.success
276      : props.warning
277      ? props.theme.colors.warning
278      : props.theme.colors.text};
279  margin-bottom: ${props => props.theme.spacing.xs};
280
281  ${props => props.pulse && `
282    animation: ${pulse} 2s infinite ease-in-out;
283  `}
284`;
285
286const StatLabel = styled.div`
287  font-size: 14px;
288  color: ${props => props.theme.colors.textMuted};
289  text-transform: uppercase;
290`;
291
292const ProgressBar = styled.div`
293  height: 8px;
294  background-color: rgba(0, 0, 0, 0.2);
295  border-radius: 4px;
296  overflow: hidden;
297  margin-bottom: ${props => props.theme.spacing.md};
298
299  &::after {
300    content: '';
301    display: block;
302    height: 100%;
303    width: ${props => `${props.value}%`};
304    background-color: ${props =>
305      props.primary
306        ? props.theme.colors.primary
307        : props.danger
308        ? props.theme.colors.danger
309        : props.success
310        ? props.theme.colors.success
311        : props.warning
312        ? props.theme.colors.warning
313        : props.theme.colors.primary};
314    transition: width 0.5s ease-in-out;
315  }
316`;
317
318const StatusIndicator = styled.span`
319  display: inline-block;
320  width: 12px;
321  height: 12px;
322  border-radius: 50%;
323  margin-right: ${props => props.theme.spacing.sm};
324  background-color: ${props =>
325    props.status === 'online'
326      ? props.theme.colors.success
327      : props.status === 'warning'
328      ? props.theme.colors.warning
329      : props.status === 'critical'
330      ? props.theme.colors.danger
331      : props.status === 'offline'
332      ? '#95a5a6'
333      : props.theme.colors.info};
334
335  ${props => props.status === 'online' && `
336    box-shadow: 0 0 10px ${props.theme.colors.success};
337  `}
338
339 ${props => props.status === 'critical' && `
340    animation: ${pulse} 1s infinite;
341  `}
342`;
343
344// ========== Composants de tableau ==========
345const Table = styled.table`
346  width: 100%;
347  border-collapse: collapse;
348`;
349
350const TableHeader = styled.th`
351  text-align: left;
352  padding: ${props => props.theme.spacing.sm};
353  border-bottom: 2px solid ${props => props.theme.colors.border};
354  color: ${props => props.theme.colors.textMuted};
355  font-weight: 500;
356`;
357
358const TableCell = styled.td`
359  padding: ${props => props.theme.spacing.sm};
360  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
361`;
362
363const TableRow = styled.tr`
364  transition: background-color 0.2s ease;
365
366  &:hover {
367    background-color: rgba(255, 255, 255, 0.05);
368  }
369`;
370
371// ========== Indicateur de chargement ==========
372const Spinner = styled.div`
373  display: inline-block;
374  width: 24px;
375  height: 24px;
376  border: 3px solid rgba(255, 255, 255, 0.3);
377  border-radius: 50%;
378  border-top-color: ${props => props.theme.colors.primary};
379  animation: ${rotate} 1s linear infinite;
380  margin-right: ${props => props.theme.spacing.sm};
381`;
382
383// ========== Composant de l'application ==========
384function SpaceDashboard() {
385  const [systemStatus, setSystemStatus] = useState('online');
386  const [fuelLevel, setFuelLevel] = useState(72);
387  const [oxygenLevel, setOxygenLevel] = useState(93);
388  const [alerts, setAlerts] = useState([
389    { id: 1, message: "Changement de pression dans le module C", severity: "warning", time: "10:23" },
390    { id: 2, message: "Mise à jour de la navigation installée", severity: "info", time: "09:15" },
391    { id: 3, message: "Particules de micrométéorites détectées", severity: "warning", time: "07:42" },
392  ]);
393
394  const crewMembers = [
395    { id: 1, name: "Alex Chen", role: "Capitaine", status: "Passerelle", avatar: "" },
396    { id: 2, name: "Maria Rodriguez", role: "Ingénieure", status: "Salle des machines", avatar: "" },
397    { id: 3, name: "Jackson Lee", role: "Navigateur", status: "Passerelle", avatar: "" },
398    { id: 4, name: "Sophia Kim", role: "Médecin", status: "Laboratoire", avatar: "" },
399  ];
400
401  const toggleSystemStatus = () => {
402    setSystemStatus(systemStatus === 'online' ? 'warning' : 'online');
403  };
404
405  return (
406    <ThemeProvider theme={theme}>
407      <GlobalStyle />
408      <AppContainer>
409        <Header>
410          <MainTitle>Centre de commandement cosmique</MainTitle>
411          <div>
412            <Button primary icon="▲" onClick={toggleSystemStatus}>
413              Système {systemStatus === 'online' ? 'En ligne' : 'Alerte'}
414            </Button>
415          </div>
416        </Header>
417
418        <Grid>
419          {/* Carte de statut système */}
420          <Card primary>
421            <CardTitle icon="◆">Statut système</CardTitle>
422            <CardContent>
423              <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
424                <div>
425                  <StatusIndicator status={systemStatus} />
426                  Statut principal : {
427                    systemStatus === 'online' ? 'Tous les systèmes opérationnels' :
428                    systemStatus === 'warning' ? 'Alerte - vérifie les alertes' :
429                    'Dysfonctionnement'
430                  }
431                </div>
432              </div>
433
434              <div>
435                <div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'space-between' }}>
436                  <span>Niveau de carburant</span>
437                  <span>{fuelLevel}%</span>
438                </div>
439                <ProgressBar
440                  value={fuelLevel}
441                  warning={fuelLevel < 50}
442                  danger={fuelLevel < 20}
443                  success={fuelLevel >= 50}
444                />
445
446                <div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'space-between' }}>
447                  <span>Niveau d'oxygène</span>
448                  <span>{oxygenLevel}%</span>
449                </div>
450                <ProgressBar
451                  value={oxygenLevel}
452                  warning={oxygenLevel < 70}
453                  danger={oxygenLevel < 40}
454                  success={oxygenLevel >= 70}
455                />
456              </div>
457            </CardContent>
458            <Button ghost primary block>Statistiques détaillées</Button>
459          </Card>
460
461          {/* Carte de statut de l'équipage */}
462          <Card success>
463            <CardTitle icon="◆">Statut de l'équipage</CardTitle>
464            <CardContent>
465              <Table>
466                <thead>
467                  <tr>
468                    <TableHeader></TableHeader>
469                    <TableHeader>Nom</TableHeader>
470                    <TableHeader>Localisation</TableHeader>
471                  </tr>
472                </thead>
473                <tbody>
474                  {crewMembers.map(member => (
475                    <TableRow key={member.id}>
476                      <TableCell>{member.avatar}</TableCell>
477                      <TableCell>
478                        {member.name}
479                        <div style={{ fontSize: '12px', color: theme.colors.textMuted }}>
480                          {member.role}
481                        </div>
482                      </TableCell>
483                      <TableCell>{member.status}</TableCell>
484                    </TableRow>
485                  ))}
486                </tbody>
487              </Table>
488            </CardContent>
489            <Button ghost success block>Gérer l'équipage</Button>
490          </Card>
491
492          {/* Carte des statistiques */}
493          <Card>
494            <CardTitle icon="◆">Statistiques</CardTitle>
495            <CardContent>
496              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
497                <Statistic>
498                  <StatValue primary>26</StatValue>
499                  <StatLabel>Jours dans l'espace</StatLabel>
500                </Statistic>
501
502                <Statistic>
503                  <StatValue success>97%</StatValue>
504                  <StatLabel>Efficacité</StatLabel>
505                </Statistic>
506
507                <Statistic>
508                  <StatValue warning pulse={fuelLevel < 30}>{fuelLevel}%</StatValue>
509                  <StatLabel>Niveau de carburant</StatLabel>
510                </Statistic>
511              </div>
512            </CardContent>
513            <Button ghost block>Voir plus</Button>
514          </Card>
515
516          {/* Carte des alertes */}
517          <Card warning={alerts.length > 0}>
518            <CardTitle icon="!">Alertes</CardTitle>
519            <CardContent>
520              {alerts.length === 0 ? (
521                <div style={{ textAlign: 'center', padding: '20px 0', color: theme.colors.textMuted }}>
522                  Aucune alerte active
523                </div>
524              ) : (
525                <div>
526                  {alerts.map(alert => (
527                    <div key={alert.id} style={{
528                      marginBottom: '8px',
529                      padding: '8px',
530                      borderRadius: '4px',
531                      backgroundColor: 'rgba(0, 0, 0, 0.2)'
532                    }}>
533                      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
534                        <div>
535                          <StatusIndicator status={
536                            alert.severity === 'critical' ? 'critical' :
537                            alert.severity === 'warning' ? 'warning' :
538                            'online'
539                          } />
540                          {alert.message}
541                        </div>
542                        <div style={{ fontSize: '12px', color: theme.colors.textMuted }}>
543                          {alert.time}
544                        </div>
545                      </div>
546                    </div>
547                  ))}
548                </div>
549              )}
550            </CardContent>
551            <Button ghost warning block>Voir toutes les alertes</Button>
552          </Card>
553        </Grid>
554
555        <WideSection>
556          <Card>
557            <CardTitle>Panneau de contrôle de mission</CardTitle>
558            <CardContent>
559              <div style={{ display: 'flex', gap: '16px', marginBottom: '20px' }}>
560                <Button primary icon="▲">Démarrer l'opération</Button>
561                <Button icon="◆">Analyser les données</Button>
562                <Button warning icon="!">Système de diagnostic</Button>
563                <Button danger icon="×">Procédure d'urgence</Button>
564              </div>
565
566              <div style={{
567                backgroundColor: 'rgba(0, 0, 0, 0.3)',
568                padding: '16px',
569                borderRadius: '8px',
570                fontFamily: theme.fonts.mono,
571                fontSize: '14px'
572              }}>
573                <div style={{ marginBottom: '8px' }}>
574                  <span style={{ color: theme.colors.success }}>system$</span> Initialisation des systèmes principaux...
575                </div>
576                <div style={{ marginBottom: '8px' }}>
577                  <span style={{ color: theme.colors.success }}>system$</span> Vérification de l'état des communications...
578                </div>
579                <div style={{ marginBottom: '8px' }}>
580                  <span style={{ color: theme.colors.success }}>system$</span> Connexion aux satellites...
581                </div>
582                <div>
583                  <Spinner /> En attente d'instructions supplémentaires...
584                </div>
585              </div>
586            </CardContent>
587          </Card>
588        </WideSection>
589      </AppContainer>
590    </ThemeProvider>
591  );
592}
593
594export default SpaceDashboard;

Une solution bien conçue : la bibliothèque de composants

L'une des meilleures approches pour gérer les styles dans les grands projets React est de créer ta propre bibliothèque de composants :

1// File: src/components/Button/Button.styles.js
2import styled, { css } from 'styled-components';
3
4// Styles réutilisables
5const buttonSizes = {
6  small: css`
7    padding: 6px 12px;
8    font-size: 12px;
9  `,
10  medium: css`
11    padding: 8px 16px;
12    font-size: 14px;
13  `,
14  large: css`
15    padding: 12px 20px;
16    font-size: 16px;
17  `
18};
19
20const buttonVariants = {
21  primary: css`
22    background-color: ${props => props.theme.colors.primary};
23    color: white;
24
25    &:hover {
26      background-color: ${props => props.theme.colors.primaryDark};
27    }
28  `,
29  secondary: css`
30    background-color: ${props => props.theme.colors.secondary};
31    color: white;
32
33    &:hover {
34      background-color: ${props => props.theme.colors.secondaryDark};
35    }
36  `,
37  success: css`
38    background-color: ${props => props.theme.colors.success};
39    color: white;
40
41    &:hover {
42      background-color: ${props => props.theme.colors.successDark};
43    }
44  `,
45  danger: css`
46    background-color: ${props => props.theme.colors.danger};
47    color: white;
48
49    &:hover {
50      background-color: ${props => props.theme.colors.dangerDark};
51    }
52  `
53};
54
55// Composant bouton principal
56export const StyledButton = styled.button`
57  display: inline-flex;
58  align-items: center;
59  justify-content: center;
60  border: none;
61  border-radius: ${props => props.theme.borderRadius.sm};
62  font-family: ${props => props.theme.fonts.body};
63  font-weight: 600;
64  cursor: pointer;
65  transition: all 0.2s ease;
66
67  // Taille par défaut
68  ${buttonSizes.medium}
69
70  // Application de la taille choisie
71  ${props => props.size && buttonSizes[props.size]}
72
73  // Variante par défaut
74  background-color: #e0e0e0;
75  color: #333;
76
77  &:hover {
78    background-color: #d5d5d5;
79  }
80
81  // Application de la variante choisie
82  ${props => props.variant && buttonVariants[props.variant]}
83
84  // Style de bouton fantôme (ghost)
85  ${props => props.ghost && css`
86    background-color: transparent;
87    border: 1px solid currentColor;
88
89    &:hover {
90      background-color: rgba(0, 0, 0, 0.05);
91    }
92  `}
93
94  // État désactivé
95  ${props => props.disabled && css`
96    opacity: 0.6;
97    cursor: not-allowed;
98    pointer-events: none;
99  `}
100
101  // Bouton pleine largeur
102  ${props => props.fullWidth && css`
103    width: 100%;
104  `}
105
106  // Prise en charge des icônes
107  ${props => props.iconPosition === 'left' && props.icon && css`
108    svg, img {
109      margin-right: 8px;
110    }
111  `}
112
113  ${props => props.iconPosition === 'right' && props.icon && css`
114    svg, img {
115      margin-left: 8px;
116    }
117  `}
118
119  // État de chargement
120  ${props => props.loading && css`
121    position: relative;
122    color: transparent;
123
124    &::after {
125      content: "";
126      position: absolute;
127      top: 50%;
128      left: 50%;
129      width: 18px;
130      height: 18px;
131      margin: -9px 0 0 -9px;
132      border: 2px solid rgba(255, 255, 255, 0.3);
133      border-radius: 50%;
134      border-top-color: white;
135      animation: buttonLoader 0.8s linear infinite;
136    }
137
138    @keyframes buttonLoader {
139      to { transform: rotate(360deg); }
140    }
141  `}
142`;
143
144// File: src/components/Button/Button.jsx
145import React from 'react';
146import PropTypes from 'prop-types';
147import { StyledButton } from './Button.styles';
148
149const Button = ({
150  children,
151  variant,
152  size,
153  disabled,
154  fullWidth,
155  ghost,
156  loading,
157  icon,
158  iconPosition = 'left',
159  as,
160  ...props
161}) => {
162  return (
163    <StyledButton
164      variant={variant}
165      size={size}
166      disabled={disabled || loading}
167      fullWidth={fullWidth}
168      ghost={ghost}
169      loading={loading}
170      icon={icon}
171      iconPosition={iconPosition}
172      as={as}
173      {...props}
174    >
175      {iconPosition === 'left' && icon}
176      {children}
177      {iconPosition === 'right' && icon}
178    </StyledButton>
179  );
180};
181
182Button.propTypes = {
183  children: PropTypes.node.isRequired,
184  variant: PropTypes.oneOf(['primary', 'secondary', 'success', 'danger']),
185  size: PropTypes.oneOf(['small', 'medium', 'large']),
186  disabled: PropTypes.bool,
187  fullWidth: PropTypes.bool,
188  ghost: PropTypes.bool,
189  loading: PropTypes.bool,
190  icon: PropTypes.node,
191  iconPosition: PropTypes.oneOf(['left', 'right']),
192  as: PropTypes.elementType
193};
194
195export default Button;
196
197// File: src/components/index.js
198export { default as Button } from './Button/Button';
199export { default as Card } from './Card/Card';
200// Autres exports...
201
202// Utilisation de la bibliothèque de composants
203import { Button, Card } from './components';
204
205function App() {
206  return (
207    <div>
208      <Card title="Panneau de contrôle">
209        <Button variant="primary" size="large">
210          Démarrer la mission
211        </Button>
212        <Button variant="danger" ghost>
213          Annuler
214        </Button>
215      </Card>
216    </div>
217  );
218}

Comparaison avec d'autres approches de style

ApprocheAvantagesInconvénients
Styled Components- Vrai CSS dans le JS<br>- Isolation des styles<br>- Styles dynamiques selon les props<br>- Prise en charge intégrée des thèmes- Augmente la taille du bundle JS<br>- Courbe d'apprentissage pour les nouveaux développeurs<br>- Les styles sont générés à l'exécution
CSS Modules- Portée CSS locale<br>- Pas de CSS-in-JS à l'exécution<br>- Syntaxe CSS familière- Styles dynamiques limités<br>- Styles conditionnels plus difficiles<br>- Nécessite une bonne configuration des outils
SCSS/SASS- Fonctionnalités puissantes (variables, mixins, etc.)<br>- Compilé au moment du build<br>- Syntaxe familière- Pas d'isolation des sélecteurs<br>- Intégration plus difficile avec les props<br>- Nécessite des outils supplémentaires
Tailwind CSS- Développement rapide<br>- Pas besoin de nommer les classes<br>- Système de design cohérent- Classes HTML surchargées<br>- Personnalisation plus difficile<br>- Courbe d'apprentissage initiale raide

Bonnes pratiques et limites

Bonnes pratiques

  1. Crée des composants de style réutilisables - évite de répéter le code CSS
  2. Utilise des thèmes - centralise les valeurs des couleurs, espacements, polices, etc.
  3. Évite l'imbrication - une imbrication profonde peut entraîner des problèmes de performance
  4. Utilise un nommage sémantique - les noms de composants doivent refléter leur fonction
  5. Utilise les props pour les variantes - au lieu de créer des composants séparés

Limites et pièges

  1. Performance - avec un grand nombre de styled components, cela peut affecter le temps de rendu
  2. Rendu côté serveur - nécessite une configuration SSR particulière
  3. Courbe d'apprentissage - les nouveaux développeurs doivent s'habituer au nouveau paradigme
  4. Débogage - les noms de classes générés automatiquement peuvent compliquer le débogage

Résumé

Styled Components est un outil puissant qui transforme l'approche du style dans React, permettant des interfaces utilisateur plus orientées composants, plus modulaires et plus réactives. Grâce à la possibilité d'écrire du vrai CSS directement dans les composants, de réagir dynamiquement aux props et de gérer facilement les thèmes, tu peux créer des systèmes de composants UI flexibles et faciles à maintenir.

Dans le monde de l'exploration cosmique de React, Styled Components est comme une combinaison spatiale avancée : elle combine tous les outils nécessaires à la survie tout en offrant liberté de mouvement et confort. Elle nous apprend que la logique et l'apparence ne sont pas forcément des entités séparées, mais qu'elles peuvent évoluer ensemble, créant une nouvelle qualité d'interfaces utilisateur.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import styles from './SpaceStation.module.css';
3
4// Démonstration des CSS Modules
5function SpaceStation() {
6  const [activeModule, setActiveModule] = useState(null);
7
8  const modules = [
9    { id: 1, name: 'Module de commandement', status: 'online', type: 'command' },
10    { id: 2, name: 'Module scientifique', status: 'online', type: 'science' },
11    { id: 3, name: "Module d'habitation", status: 'online', type: 'habitat' },
12    { id: 4, name: 'Module énergétique', status: 'warning', type: 'power' },
13    { id: 5, name: "Module d'amarrage", status: 'offline', type: 'docking' }
14  ];
15
16  const getStatusClass = (status) => {
17    switch(status) {
18      case 'online': return styles.statusOnline;
19      case 'warning': return styles.statusWarning;
20      case 'offline': return styles.statusOffline;
21      default: return '';
22    }
23  };
24
25  return (
26    <div className={styles.station}>
27      <header className={styles.header}>
28        <h1 className={styles.title}>Station spatiale Orion</h1>
29        <p className={styles.subtitle}>Système de gestion des modules</p>
30      </header>
31
32      <div className={styles.dashboard}>
33        <div className={styles.moduleList}>
34          <h2 className={styles.sectionTitle}>Modules de la station</h2>
35
36          {modules.map(module => (
37            <div
38              key={module.id}
39              className={`${styles.moduleCard} ${activeModule === module.id ? styles.active : ''}`}
40              onClick={() => setActiveModule(module.id)}
41            >
42              <div className={styles.moduleHeader}>
43                <span className={styles.moduleName}>{module.name}</span>
44                <span className={`${styles.status} ${getStatusClass(module.status)}`}>
45                  {module.status}
46                </span>
47              </div>
48
49              <div className={styles.moduleInfo}>
50                <span className={styles.moduleType}>Type : {module.type}</span>
51              </div>
52            </div>
53          ))}
54        </div>
55
56        <div className={styles.details}>
57          <h2 className={styles.sectionTitle}>Panneau de détails</h2>
58
59          {activeModule ? (
60            <div className={styles.detailsContent}>
61              <p>Module sélectionné : <strong>{modules.find(m => m.id === activeModule)?.name}</strong></p>
62              <p>État des systèmes : tous les systèmes fonctionnent correctement</p>
63
64              <div className={styles.actions}>
65                <button className={styles.buttonPrimary}>Diagnostyka</button>
66                <button className={styles.buttonSecondary}>Raport</button>
67              </div>
68            </div>
69          ) : (
70            <p className={styles.placeholder}>Sélectionne un module dans la liste pour voir les détails</p>
71          )}
72        </div>
73      </div>
74
75      <footer className={styles.footer}>
76        <p>Les CSS Modules garantissent une portée locale pour les classes CSS</p>
77      </footer>
78    </div>
79  );
80}
81
82export default SpaceStation;

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. Comment passe-t-on des valeurs dynamiques aux styled-components en fonction des props ?

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

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne la syntaxe pour créer un styled-component :

  • Éditeur de code

    Styled Components - les bases de la création de composants avec des styles en JavaScript

  • Tri vertical

    Classe les étapes pour ajouter des styles dynamiques dans styled-components :

  • Éditeur de code

    Styled Components ThemeProvider - système de thèmes avec bascule dynamique mode sombre/clair

  • Tri horizontal

    Ordonne la syntaxe correcte d'un styled-component avec une couleur dynamique :

  • Tri vertical

    Classe les méthodes de style du couplage le plus faible au plus fort avec le composant :

Articles utiles