Cours Vue.js · Module 6: Composants
Styles scoped et CSS Modules
Dans cette leçon5
L'isolation des styles dans les composants fonctionne comme les cloisons étanches entre les modules de l'habitat : ce qui se passe dans un compartiment ne passe pas dans les compartiments voisins.
Deux composants de la base utilisent une classe .card : l'un est une carte de module, l'autre une carte de membre d'équipage. Le CSS ordinaire s'applique à toute la page, donc une règle écrite dans un fichier repeint les cartes des deux. Vue propose plusieurs niveaux d'isolation, et aujourd'hui tu vas découvrir chacun d'eux.
Styles scoped
Tu connais l'attribut scoped depuis la Launch Zone : les règles d'un tel bloc ne s'appliquent qu'aux éléments du composant courant. Commençons par une carte dont les styles ne fuiront pas hors du fichier :
1<template>
2 <div class="card">
3 <h3 class="title">{{ title }}</h3>
4 </div>
5</template>
6
7<style scoped>
8/* Uniquement pour ce composant */
9.card {
10 border: 1px solid #ccc;
11 padding: 1rem;
12}
13
14.title {
15 color: #333;
16}
17</style>Vue ajoute à chaque élément du composant un attribut comme data-v-7ba5bd90 et transforme le sélecteur .card en .card[data-v-7ba5bd90]. Une carte de membre d'équipage dans un autre fichier reçoit un autre attribut, donc cette règle ne la touchera pas. Il existe une exception voulue : l'élément racine d'un composant enfant subit à la fois les styles de l'enfant et les styles scoped du parent, pour que le parent puisse le placer dans sa mise en page.
:deep() - un regard à l'intérieur de l'enfant
Parfois, pourtant, le parent doit styliser un élément situé plus profondément dans un composant enfant. C'est le rôle du pseudo-sélecteur :deep() :
1<style scoped>
2/* Sélecteur profond - agir sur les composants enfants */
3.card :deep(.child-class) {
4 color: blue;
5}
6</style>Le compilateur transforme cette règle en .card[data-v-7ba5bd90] .child-class. L'attribut ne reste que sur .card, donc .child-class correspond aussi aux éléments à l'intérieur des enfants. Les anciennes écritures >>> et /deep/ viennent de Vue 2 - le compilateur de Vue 3 les comprend encore, mais il prévient qu'elles sont dépréciées et demande d'utiliser :deep().
:slotted() et :global()
Deux autres pseudo-sélecteurs concernent du contenu venu de l'extérieur du composant. Un slot est un emplacement du template de l'enfant dans lequel le parent place son propre contenu - tu le découvriras à l'Entrepôt des Modules. Ce contenu appartient au parent, donc les styles scoped de l'enfant ne le couvrent pas par défaut :
1<style scoped>
2/* Contenu injecté via slot */
3:slotted(.slot-class) {
4 font-weight: bold;
5}
6
7/* Global dans un style scoped */
8:global(.global-class) {
9 background: yellow;
10}
11</style>:slotted() dirige une règle vers le contenu inséré par un slot, tandis que :global() désactive l'isolation pour une seule règle, donc .global-class agira sur toute la page. Utilise les règles globales dans un style scoped avec parcimonie, car c'est une porte ouverte dans la cloison.
CSS Modules
La deuxième façon d'isoler les styles, ce sont les CSS Modules. Un bloc <style module> transforme chaque classe en un nom unique et généré, et dans le template tu y accèdes par l'objet $style et la directive :class :
1<template>
2 <div :class="$style.card">
3 <h3 :class="$style.title">{{ title }}</h3>
4 <p :class="[$style.text, $style.highlighted]">Description</p>
5 </div>
6</template>
7
8<style module>
9.card {
10 border: 1px solid #ccc;
11}
12
13.title {
14 color: #333;
15}
16
17.text {
18 font-size: 14px;
19}
20
21.highlighted {
22 background: yellow;
23}
24</style>Les crochets appliquent deux classes à la fois, car :class accepte aussi un tableau. La classe card de ce fichier n'a rien à voir avec card dans un autre composant - les deux reçoivent des noms différents. La différence avec scoped : l'isolation passe par les noms de classes et non par des attributs, et elle ne couvre que les éléments auxquels tu attribues explicitement une classe de $style. Tu peux aussi lire les noms générés dans le script avec la fonction useCssModule :
1<script setup>
2import { useCssModule } from 'vue'
3
4const styles = useCssModule()
5console.log(styles.card) // par ex. "_card_1a2b3_2" - le nom est généré par Vite
6</script>Le format du nom dépend de la configuration de l'outil de build - par défaut, Vite combine le nom de la classe, une empreinte du contenu et le numéro de ligne. Un bloc peut aussi recevoir son propre nom, par exemple <style module="classes">, et tu écris alors classes.card dans le template.
Styles dynamiques
Parfois, une couleur doit dépendre des données, par exemple du type de module. La directive :style accepte un objet, et ses clés peuvent aussi être des variables CSS, c'est-à-dire des propriétés qui commencent par deux tirets :
1<template>
2 <div
3 class="card"
4 :style="{
5 '--primary-color': primaryColor,
6 '--border-width': borderWidth + 'px'
7 }"
8 >
9 <h3>{{ title }}</h3>
10 </div>
11</template>
12
13<script setup>
14import { ref } from 'vue'
15
16const primaryColor = ref('#8b7355')
17const borderWidth = ref(2)
18</script>
19
20<style scoped>
21.card {
22 border: var(--border-width) solid var(--primary-color);
23 color: var(--primary-color);
24}
25</style>Le composant définit les variables --primary-color et --border-width sur son élément, et la règle de <style scoped> les lit avec la fonction var(). La feuille de style ne change pas - seules les valeurs des variables changent quand les conteneurs ref() changent.
v-bind() dans le CSS
Vue sait faire la même chose plus simplement : la fonction v-bind() écrite directement dans une règle CSS relie une valeur à un conteneur de <script setup> :
1<template>
2 <div class="card">
3 <input v-model="color" type="color" />
4 <p>Ce texte change de couleur !</p>
5 </div>
6</template>
7
8<script setup>
9import { ref } from 'vue'
10
11const color = ref('#8b7355')
12</script>
13
14<style scoped>
15p {
16 /* Liaison réactive ! */
17 color: v-bind(color);
18}
19</style>Choisis une couleur dans le champ, et le texte la prend aussitôt. Le compilateur transforme v-bind(color) en une variable CSS au nom unique, que Vue place sur l'élément racine du composant et met à jour à chaque changement, si bien que la feuille de style elle-même reste statique.
Les niveaux d'isolation
De la portée la plus large à la plus étroite : les styles globaux sans scoped s'appliquent à toute la page, <style scoped> aux éléments d'un seul composant, les CSS Modules uniquement aux éléments qui portent une classe de $style, et un style inline via :style à un seul élément précis. Au NOVA LAB, nous écrivons <style scoped> par défaut et nous passons aux CSS Modules quand les noms de classes sont nécessaires dans le script - c'est le point de départ que je te recommande.
Dans l'éditeur ci-dessous, chaque composant du panneau de réglage des matériaux a son propre bloc <style scoped>. Change une couleur dans un fichier et vérifie que l'autre reste inchangé. Le panneau utilise aussi v-model sur des composants, que tu découvriras à la Station de Communication.
Retiens ceci : scoped pose des cloisons autour d'un module, les CSS Modules donnent aux classes des noms uniques, et v-bind() dans le CSS branche l'apparence directement sur les données de la mission.
Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3import MaterialSettings from './MaterialSettings.vue'
4
5const printSettings = ref({
6 material: 'PLA',
7 temperature: 210,
8 speed: 50,
9 layerHeight: 0.2
10})
11
12const extruderSettings = ref({
13 flowRate: 100,
14 retraction: 5
15})
16
17function savePrintSettings() {
18 console.log("Paramètres d'impression enregistrés :", printSettings.value)
19 alert('Paramètres enregistrés !')
20}
21
22function saveExtruderSettings() {
23 console.log("Paramètres de l'extrudeuse enregistrés :", extruderSettings.value)
24 alert("Paramètres de l'extrudeuse enregistrés !")
25}
26</script>
27
28<template>
29 <div class="settings-lab">
30 <div class="header">
31 <h1>Material Settings - NOVA LAB</h1>
32 <p class="subtitle">Component v-model</p>
33 </div>
34
35 <div class="info-panel">
36 <h3>v-model dans les composants</h3>
37 <p><strong>v-model</strong> est un raccourci pour :modelValue + @update:modelValue</p>
38 <p>Tu peux utiliser plusieurs v-model avec des noms différents : v-model:customName</p>
39 <code class="code-example">
40<!-- Dans le composant parent -->
41v-model="value"
42v-model:customName="otherValue"
43
44<!-- Dans le composant enfant -->
45defineProps(['modelValue', 'customName'])
46emit('update:modelValue', newValue)
47emit('update:customName', newValue)
48 </code>
49 </div>
50
51 <div class="settings-grid">
52 <!-- v-model unique -->
53 <div class="settings-card">
54 <h2>Paramètres d'impression de base</h2>
55 <MaterialSettings v-model="printSettings" />
56
57 <div class="preview-section">
58 <h4>Aperçu de la valeur :</h4>
59 <pre>{{ JSON.stringify(printSettings, null, 2) }}</pre>
60 <button @click="savePrintSettings" class="btn btn-save">
61 Enregistrer les paramètres
62 </button>
63 </div>
64 </div>
65
66 <!-- v-model multiples -->
67 <div class="settings-card">
68 <h2>Paramètres de l'extrudeuse</h2>
69 <MaterialSettings
70 v-model="extruderSettings"
71 :show-advanced="true"
72 />
73
74 <div class="preview-section">
75 <h4>Aperçu de la valeur :</h4>
76 <pre>{{ JSON.stringify(extruderSettings, null, 2) }}</pre>
77 <button @click="saveExtruderSettings" class="btn btn-save">
78 Enregistrer les paramètres de l'extrudeuse
79 </button>
80 </div>
81 </div>
82
83 <!-- Saisie directe avec v-model -->
84 <div class="settings-card">
85 <h2>Édition manuelle (JSON)</h2>
86 <textarea
87 v-model="printSettings.material"
88 class="json-editor"
89 placeholder="Modifier le nom du matériau..."
90 ></textarea>
91 <p class="info-text">
92 Les modifications du champ texte mettent automatiquement à jour l'objet printSettings
93 </p>
94 </div>
95 </div>
96
97 <div class="summary-panel">
98 <h2>Récapitulatif de tous les paramètres</h2>
99 <div class="summary-grid">
100 <div class="summary-item">
101 <h4>Paramètres d'impression</h4>
102 <ul>
103 <li>Matériau : <strong>{{ printSettings.material }}</strong></li>
104 <li>Température : <strong>{{ printSettings.temperature }}°C</strong></li>
105 <li>Vitesse : <strong>{{ printSettings.speed }}%</strong></li>
106 <li>Hauteur de couche : <strong>{{ printSettings.layerHeight }}mm</strong></li>
107 </ul>
108 </div>
109 <div class="summary-item">
110 <h4>Paramètres de l'extrudeuse</h4>
111 <ul>
112 <li>Débit : <strong>{{ extruderSettings.flowRate }}%</strong></li>
113 <li>Rétraction : <strong>{{ extruderSettings.retraction }}mm</strong></li>
114 </ul>
115 </div>
116 </div>
117 </div>
118 </div>
119</template>
120
121<style scoped>
122.settings-lab {
123 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
124 min-height: 100vh;
125 padding: 2rem;
126 color: #ffffff;
127 font-family: 'Courier New', monospace;
128}
129
130.header {
131 text-align: center;
132 margin-bottom: 2rem;
133 padding-bottom: 1rem;
134 border-bottom: 2px solid #00ff88;
135}
136
137.header h1 {
138 font-size: 2.5rem;
139 color: #00ff88;
140 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
141 margin: 0;
142}
143
144.subtitle {
145 color: #00b4d8;
146 font-size: 1.2rem;
147 margin-top: 0.5rem;
148}
149
150.info-panel {
151 background: rgba(0, 180, 216, 0.1);
152 border: 2px solid #00b4d8;
153 border-radius: 10px;
154 padding: 1.5rem;
155 margin-bottom: 2rem;
156}
157
158.info-panel h3 {
159 color: #00b4d8;
160 margin-top: 0;
161}
162
163.info-panel strong {
164 color: #00ff88;
165}
166
167.code-example {
168 display: block;
169 background: rgba(0, 0, 0, 0.5);
170 border-left: 4px solid #00ff88;
171 padding: 1rem;
172 border-radius: 5px;
173 color: #00ff88;
174 margin-top: 1rem;
175 white-space: pre;
176 overflow-x: auto;
177}
178
179.settings-grid {
180 display: grid;
181 grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
182 gap: 1.5rem;
183 margin-bottom: 2rem;
184}
185
186.settings-card {
187 background: rgba(0, 255, 136, 0.05);
188 border: 2px solid #00ff88;
189 border-radius: 10px;
190 padding: 1.5rem;
191}
192
193.settings-card h2 {
194 color: #00ff88;
195 margin-top: 0;
196 margin-bottom: 1.5rem;
197}
198
199.preview-section {
200 margin-top: 1.5rem;
201 padding-top: 1.5rem;
202 border-top: 2px solid rgba(0, 255, 136, 0.2);
203}
204
205.preview-section h4 {
206 color: #00b4d8;
207 margin-top: 0;
208}
209
210.preview-section pre {
211 background: rgba(0, 0, 0, 0.5);
212 padding: 1rem;
213 border-radius: 5px;
214 color: #00ff88;
215 overflow-x: auto;
216 border-left: 4px solid #00ff88;
217}
218
219.json-editor {
220 width: 100%;
221 min-height: 150px;
222 background: rgba(0, 0, 0, 0.3);
223 border: 2px solid #00b4d8;
224 border-radius: 5px;
225 padding: 1rem;
226 color: #ffffff;
227 font-family: 'Courier New', monospace;
228 resize: vertical;
229}
230
231.json-editor:focus {
232 outline: none;
233 border-color: #00ff88;
234 box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
235}
236
237.info-text {
238 color: #00b4d8;
239 font-size: 0.9rem;
240 margin-top: 1rem;
241}
242
243.btn-save {
244 background: #00ff88;
245 color: #0a0e27;
246 border: none;
247 padding: 0.75rem 1.5rem;
248 border-radius: 5px;
249 font-weight: bold;
250 cursor: pointer;
251 transition: all 0.3s ease;
252 font-family: 'Courier New', monospace;
253 margin-top: 1rem;
254}
255
256.btn-save:hover {
257 transform: scale(1.05);
258 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
259}
260
261.summary-panel {
262 background: rgba(0, 180, 216, 0.1);
263 border: 2px solid #00b4d8;
264 border-radius: 10px;
265 padding: 1.5rem;
266}
267
268.summary-panel h2 {
269 color: #00b4d8;
270 margin-top: 0;
271}
272
273.summary-grid {
274 display: grid;
275 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
276 gap: 1.5rem;
277}
278
279.summary-item {
280 background: rgba(0, 0, 0, 0.3);
281 padding: 1.5rem;
282 border-radius: 8px;
283 border-left: 4px solid #00ff88;
284}
285
286.summary-item h4 {
287 color: #00ff88;
288 margin-top: 0;
289}
290
291.summary-item ul {
292 list-style: none;
293 padding: 0;
294}
295
296.summary-item li {
297 padding: 0.5rem 0;
298 color: #ffffff;
299 border-bottom: 1px solid rgba(0, 255, 136, 0.1);
300}
301
302.summary-item li:last-child {
303 border-bottom: none;
304}
305
306.summary-item strong {
307 color: #00b4d8;
308}
309</style>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. Que fait l'attribut scoped dans <style> ?
2. Comment styliser les composants enfants dans des styles scoped ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Ajoute des styles scoped à MarsPanel.vue avec des classes pour le panneau (.panel), le titre (.title) et le statut (.status).
- Clique dans l'ordre
Range les éléments d'une déclaration de style scoped :
- Tri vertical
Range les étapes pour créer un composant, de la création à l'utilisation :