Cours JavaScript et React · Module 14: Styles avancés
Projet principal : Dashboard cosmique avec différentes approches de stylage
Dans cette leçon5
Dans les vrais projets, tu tomberas rarement sur du code stylé avec un seul outil. L'ancien layout a été écrit en CSS Modules, quelqu'un a ajouté styled-components pour les cartes dynamiques, et la nouvelle équipe assemble rapidement des vues avec Tailwind. Savoir naviguer entre ces mondes, c'est ce qui distingue un junior d'un ingénieur capable de piloter n'importe quel vaisseau de la flotte. Il est temps de faire la synthèse ! Dans ce projet, tu vas construire un Dashboard de mission cosmique qui combine différentes approches de stylage. C'est comme construire un panneau de contrôle de mission complet où chaque secteur utilise le meilleur outil pour sa tâche.
Objectif du projet
Créer un dashboard qui affiche des informations sur les missions spatiales. Le projet doit démontrer ta capacité à appliquer différentes méthodes de stylage au sein d'un seul projet. L'essentiel est de répartir les rôles de façon consciente : chaque outil fait ce qu'il fait de mieux et ne gêne pas les autres.
Exigences
1. Disposition de la page (CSS Modules)
La disposition principale du dashboard - sidebar, header, zone principale - doit être stylisée à l'aide des CSS Modules. Rappelle-toi qu'un fichier avec l'extension .module.css est du CSS ordinaire, dans lequel chaque nom de classe reçoit un suffixe unique au moment du build, donc .container n'entrera en collision avec aucune autre classe de l'application. La grille a deux colonnes (la sidebar de 250px et le reste) et deux lignes (le header de 60px et le contenu) :
1/* Dashboard.module.css */
2.container {
3 display: grid;
4 grid-template-columns: 250px 1fr;
5 grid-template-rows: 60px 1fr;
6 min-height: 100vh;
7 background: #0a0a2e;
8}
9
10.sidebar {
11 grid-row: 1 / -1;
12 background: #0d1137;
13 padding: 20px;
14}
15
16.header {
17 background: #1a1a4e;
18 display: flex;
19 align-items: center;
20 padding: 0 24px;
21}
22
23.main {
24 padding: 24px;
25 overflow-y: auto;
26}La règle grid-row: 1 / -1 étend la sidebar sur toutes les lignes, de la première à la dernière ligne de la grille. Dans le composant, tu utiliseras ces classes en important styles et en écrivant className={styles.container}, comme dans la leçon sur les CSS Modules.
2. Composants interactifs (Styled-components)
Boutons, badges de statut, cartes avec des styles dynamiques - c'est là que styled-components brille, car le style dépend des props. Remarque le signe dollar dans $status. C'est ce qu'on appelle une transient prop : styled-components en version 6 ne filtre plus automatiquement les props inconnues et transmettrait un simple status à l'élément span dans le DOM (en mode développement, il affiche alors un avertissement). Le préfixe $ indique à la bibliothèque que la prop sert uniquement au stylage :
1const StatusBadge = styled.span`
2 padding: 4px 12px;
3 border-radius: 20px;
4 font-size: 12px;
5 font-weight: bold;
6 background: ${({ $status, theme }) => theme.statusColors[$status]};
7 color: ${({ $status }) => $status === 'active' ? '#0a0a2e' : 'white'};
8`;Tu l'utiliseras sous la forme <StatusBadge $status="active">. La couleur de fond vient du thème (theme.statusColors), donc ajouter un nouveau statut demande seulement une nouvelle clé dans le thème, et non une modification du composant.
3. Cartes et listes (Tailwind CSS)
Des composants de présentation rapides avec des classes utilitaires. Chaque classe Tailwind fait une seule chose : flex active le flexbox, gap-3 définit l'espacement, rounded-lg arrondit les coins, et hover:bg-slate-700 change le fond au survol :
1function CrewMember({ name, role, avatar }) {
2 return (
3 <div className="flex items-center gap-3 p-3 bg-slate-800 rounded-lg
4 hover:bg-slate-700 transition-colors">
5 <img src={avatar} className="w-10 h-10 rounded-full" alt={name} />
6 <div>
7 <p className="text-white font-medium">{name}</p>
8 <p className="text-gray-400 text-sm">{role}</p>
9 </div>
10 </div>
11 );
12}Il n'y a ici aucun fichier CSS ni styled-component, toute l'apparence se trouve dans className. C'est parfait pour des morceaux d'interface simples et statiques, mais cela convient moins bien aux styles qui dépendent fortement de l'état.
4. Theming (ThemeProvider de Styled-components)
Tout le dashboard doit utiliser un thème de couleurs partagé. L'objet contient les colors que tu connais déjà et un nouveau statusColors, utilisé par StatusBadge :
1const spaceTheme = {
2 colors: {
3 background: '#0a0a2e',
4 surface: '#1a1a4e',
5 primary: '#7c4dff',
6 text: '#e0e0e0',
7 success: '#00e676',
8 warning: '#ffa726',
9 danger: '#f44336'
10 },
11 statusColors: {
12 active: '#00e676',
13 pending: '#ffa726',
14 completed: '#64b5f6',
15 failed: '#f44336'
16 }
17};Seuls les styled-components placés à l'intérieur de ThemeProvider voient le thème. Les classes des CSS Modules et de Tailwind ne le connaissent pas, c'est pourquoi, si tu veux une seule source de couleurs pour les trois approches, envisage d'exposer les mêmes valeurs sous forme de CSS Variables.
5. Responsivité
Le dashboard doit être responsive - sur les écrans plus petits, la sidebar se cache, et la grille de cartes passe à une disposition en colonne. Dans le même fichier CSS Modules, on ajoute une media query :
1/* Dashboard.module.css */
2@media (max-width: 768px) {
3 .container {
4 grid-template-columns: 1fr;
5 }
6 .sidebar {
7 display: none;
8 }
9}En dessous de 768px, la grille n'a plus qu'une colonne et la sidebar disparaît. Les autres règles de .container du premier bloc restent valables, on ne surcharge que ce qui change. Dans un vrai projet, au lieu de cacher complètement la navigation, ajoute un bouton qui la fait glisser.
Structure des composants
Voici l'arbre de toute l'application. À côté de chaque composant, l'outil avec lequel tu le styles est indiqué entre parenthèses :
1App
2 ThemeProvider (spaceTheme)
3 GlobalStyles
4 DashboardLayout (CSS Modules)
5 Sidebar (CSS Modules)
6 NavItem (CSS Modules)
7 Header (CSS Modules + Styled StatusBadge)
8 MainContent
9 MissionGrid (Tailwind grid)
10 MissionCard (Styled-components, props dynamiques)
11 StatusBadge (Styled, changement de couleur selon le statut)
12 CrewList (Tailwind)
13 CrewMember (Tailwind)ThemeProvider et GlobalStyles se trouvent tout en haut, pour que le thème atteigne chaque carte et chaque badge, quelle que soit la section où ils se trouvent.
Conseils
- Commence par la disposition (CSS Modules) - crée une grille avec sidebar, header et main
- Ajoute le composant ThemeProvider et GlobalStyles
- Construis les composants interactifs (cartes de mission, badges de statut) avec styled-components
- Utilise les classes Tailwind pour les éléments de présentation rapides (liste d'équipage, grille de cartes)
- Souviens-toi de la cohérence des couleurs - toutes les couleurs doivent provenir du thème
- Ajoute au moins un effet hover/focus et une animation
Mon conseil : dans un même composant, tiens-toi à un seul outil. Mélanger des className Tailwind et des styled-components sur le même élément devient vite illisible.
Critères d'évaluation
- CSS Modules utilisés pour la disposition (grille, sidebar, header)
- Styled-components utilisés pour au moins 3 composants avec des props dynamiques
- Tailwind CSS utilisé pour au moins 2 sections de présentation
- ThemeProvider avec un thème de couleurs défini
- Responsivité - le dashboard fonctionne sur différentes tailles d'écran
Ci-dessous, tu trouveras un exemple de dashboard terminé qui combine les trois approches. Analyse le code et inspire-toi-en pour créer ta propre version ! Tu enverras le projet terminé à ton mentor dans la tâche suivante, et dans les prochains lieux, ces mêmes règles de répartition des rôles te serviront pour chaque application plus importante.
Retiens : un bon tableau de bord de mission, ce n'est pas un seul outil pour tout, mais chaque secteur équipé de ce qui y fonctionne le mieux.
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3import layoutStyles from './Dashboard.module.css';
4
5// === THEME (styled-components) ===
6const theme = {
7 colors: {
8 primary: '#7c4dff',
9 secondary: '#64b5f6',
10 bg: '#0a0a2e',
11 surface: '#1a1a4e',
12 text: '#e0e0e0',
13 muted: '#9e9e9e',
14 border: '#3949ab',
15 success: '#00e676',
16 warning: '#ffa726',
17 danger: '#f44336',
18 },
19};
20
21const GlobalStyles = createGlobalStyle`
22 * { margin: 0; padding: 0; box-sizing: border-box; }
23 body { font-family: 'Segoe UI', sans-serif; }
24`;
25
26// === STYLED-COMPONENTS (composants dynamiques) ===
27const StatusDot = styled.span`
28 display: inline-block;
29 width: 10px; height: 10px;
30 border-radius: 50%;
31 margin-right: 8px;
32 background: ${({ status, theme }) =>
33 status === 'active' ? theme.colors.success :
34 status === 'warning' ? theme.colors.warning :
35 theme.colors.danger};
36`;
37
38const StyledButton = styled.button`
39 padding: 8px 16px;
40 border: none;
41 border-radius: 6px;
42 cursor: pointer;
43 font-weight: bold;
44 font-size: 13px;
45 transition: all 0.2s;
46 background: ${({ variant, theme }) =>
47 variant === 'primary' ? theme.colors.primary :
48 variant === 'danger' ? theme.colors.danger : 'transparent'};
49 color: ${({ variant }) => variant === 'ghost' ? '#64b5f6' : 'white'};
50 border: ${({ variant }) => variant === 'ghost' ? '1px solid #3949ab' : 'none'};
51 &:hover { opacity: 0.85; }
52`;
53
54const ProgressBar = styled.div`
55 height: 6px;
56 border-radius: 3px;
57 background: #0d1137;
58 margin-top: 8px;
59 overflow: hidden;
60 &::after {
61 content: '';
62 display: block;
63 height: 100%;
64 width: ${({ value }) => value}%;
65 background: ${({ value, theme }) =>
66 value > 70 ? theme.colors.success :
67 value > 40 ? theme.colors.warning :
68 theme.colors.danger};
69 transition: width 0.5s ease;
70 }
71`;
72
73// === DONNÉES ===
74const missions = [
75 { id: 1, name: 'Apollo X', target: 'Mars', status: 'active', progress: 72, crew: ['Nova', 'Rex', 'Luna'] },
76 { id: 2, name: 'Nebula Run', target: 'Orion', status: 'warning', progress: 45, crew: ['Astro', 'Vega'] },
77 { id: 3, name: 'Deep Void', target: 'Andromeda', status: 'critical', progress: 18, crew: ['Echo', 'Titan', 'Zara', 'Sol'] },
78];
79
80const navItems = ['Dashboard', 'Missions', 'Crew', 'Systems', 'Reports'];
81
82// === COMPOSANT PRINCIPAL ===
83export default function App() {
84 const [activeNav, setActiveNav] = useState('Dashboard');
85 const [selectedMission, setSelectedMission] = useState(null);
86
87 return (
88 <ThemeProvider theme={theme}>
89 <GlobalStyles />
90 <div className={layoutStyles.container}>
91 {/* Sidebar (CSS Modules) */}
92 <aside className={layoutStyles.sidebar}>
93 <h2 className={layoutStyles.logo}>Mission Control</h2>
94 <nav>
95 {navItems.map(item => (
96 <div
97 key={item}
98 className={`${layoutStyles.navItem} ${activeNav === item ? layoutStyles.navActive : ''}`}
99 onClick={() => setActiveNav(item)}
100 >
101 {item}
102 </div>
103 ))}
104 </nav>
105 </aside>
106
107 {/* Header (CSS Modules) */}
108 <header className={layoutStyles.header}>
109 <h1 className={layoutStyles.headerTitle}>Dashboard cosmique</h1>
110 <span className={layoutStyles.headerBadge}>3 active missions</span>
111 </header>
112
113 {/* Main content (mix) */}
114 <main className={layoutStyles.main}>
115 <div className={layoutStyles.grid}>
116 {missions.map(mission => (
117 <div
118 key={mission.id}
119 className={layoutStyles.card}
120 onClick={() => setSelectedMission(
121 selectedMission === mission.id ? null : mission.id
122 )}
123 >
124 <div className={layoutStyles.cardHeader}>
125 <h3 className={layoutStyles.cardTitle}>
126 <StatusDot status={mission.status} />
127 {mission.name}
128 </h3>
129 <span className={layoutStyles.cardTarget}>{mission.target}</span>
130 </div>
131 <p className={layoutStyles.cardMuted}>
132 Équipage : {mission.crew.length} membres
133 </p>
134 <ProgressBar value={mission.progress} />
135 <p className={layoutStyles.progressLabel}>{mission.progress}%</p>
136
137 {selectedMission === mission.id && (
138 <div className={layoutStyles.cardDetails}>
139 <p className={layoutStyles.detailTitle}>Membres de l'équipage :</p>
140 {mission.crew.map(c => (
141 <span key={c} className={layoutStyles.crewBadge}>{c}</span>
142 ))}
143 <div className={layoutStyles.buttonRow}>
144 <StyledButton variant="primary">Détails</StyledButton>
145 <StyledButton variant="ghost">Rapport</StyledButton>
146 {mission.status === 'critical' && (
147 <StyledButton variant="danger">Abort</StyledButton>
148 )}
149 </div>
150 </div>
151 )}
152 </div>
153 ))}
154 </div>
155 </main>
156 </div>
157 </ThemeProvider>
158 );
159}Tu as repéré une erreur dans cette leçon ?