Cours JavaScript et React · Module 7: Styles et animations
Styled Components - Fusion cosmique de JSX et de CSS
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-componentsPour les utilisateurs de TypeScript, il vaut la peine d'ajouter aussi les types :
1npm install @types/styled-componentsLes 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>×</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
| Approche | Avantages | Inconvé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
- Crée des composants de style réutilisables - évite de répéter le code CSS
- Utilise des thèmes - centralise les valeurs des couleurs, espacements, polices, etc.
- Évite l'imbrication - une imbrication profonde peut entraîner des problèmes de performance
- Utilise un nommage sémantique - les noms de composants doivent refléter leur fonction
- Utilise les props pour les variantes - au lieu de créer des composants séparés
Limites et pièges
- Performance - avec un grand nombre de styled components, cela peut affecter le temps de rendu
- Rendu côté serveur - nécessite une configuration SSR particulière
- Courbe d'apprentissage - les nouveaux développeurs doivent s'habituer au nouveau paradigme
- 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. Comment passe-t-on des valeurs dynamiques aux styled-components en fonction des props ?
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 :