Vue.js course ยท Module 2: Template Syntax
Conditional Directives
In this lesson4
Just as NOVA LAB systems make decisions based on sensor readings, Vue allows you to control what appears in the interface.
The classes from the previous lesson change how an element looks, but the element still exists. Sometimes the station hologram needs something stronger: show the panel of only the system the operator picked, and display the failure message only during a failure. That is what the conditional directives are for - v-if, which decides whether the element is created at all, and v-show, which only hides it.
v-if, v-else-if, v-else
v-if renders an element only when the expression is truthy. v-else-if adds further conditions, and v-else, which takes no value at all, catches the rest. Both must come immediately after an element with v-if or v-else-if. A <template> tag with v-if lets you put a whole group of elements under one condition:
1<template>
2 <div class="system-monitor" style="background: #0a0e27; color: #00ff88;">
3 <div v-if="systemType === 'oxygen'">
4 Oxygen generator - System active
5 </div>
6 <div v-else-if="systemType === 'water'">
7 Water recycling - System active
8 </div>
9 <div v-else-if="systemType === 'power'">
10 Plasma reactor - System active
11 </div>
12 <div v-else>
13 Unknown system
14 </div>
15
16 <!-- On a group of elements -->
17 <template v-if="showDetails">
18 <h2 style="color: #00b4d8;">{{ module.name }}</h2>
19 <p>{{ module.description }}</p>
20 <span>Status: {{ module.status }}</span>
21 </template>
22 </div>
23</template>With systemType equal to 'oxygen', only the first div exists in the DOM. The others are not hidden - they simply are not there. The <template> itself is not rendered either: the heading, the paragraph and the span land on the page without an extra wrapper. If another element stood between v-if and v-else, the compiler would report that v-else has no adjacent v-if.
The conditions read ordinary data from the script:
1<script setup>
2import { ref, reactive } from 'vue'
3
4const systemType = ref('oxygen')
5const showDetails = ref(true)
6
7const module = reactive({
8 name: 'Life Support Alpha',
9 description: 'Primary oxygen generation system',
10 status: 'ONLINE'
11})
12</script>Set systemType.value = 'water', and Vue removes the oxygen element and creates the water one. On every render Vue builds a new Virtual DOM tree, a lightweight description of the page in JavaScript. When the condition is false, the element is missing from that tree. Vue compares the new tree with the previous one and only then adds or removes the real element in the browser. v-if is also lazy: if the condition is false at the start, nothing is rendered, and on every toggle the block's content is destroyed and created from scratch.
v-show
v-show works differently: the element is always rendered, and the directive only toggles the CSS display property.
1<template>
2 <div class="alert-panel">
3 <!-- v-show - always rendered, just CSS display: none -->
4 <div v-show="isVisible" class="alert-message"
5 style="background: #ff4444; color: white; padding: 1rem;">
6 CRITICAL ALERT: Oxygen levels dropping!
7 </div>
8 </div>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const isVisible = ref(true)
15</script>With isVisible set to false, the element gets style="display: none;", but it stays in the DOM together with its state. v-show does not work on a <template> tag and does not pair with v-else. Most often it is driven by a button that flips a boolean value:
1<template>
2 <button @click="isVisible = !isVisible">Toggle panel</button>
3 <div v-show="isVisible">Diagnostics panel</div>
4</template>Every click turns true into false and back. It is the same in-template statement you met with v-on.
v-if vs v-show
Both directives produce the same effect on screen, but they cost different things:
| Feature | v-if | v-show |
|---|---|---|
| Rendering | Conditional | Always |
| Initial cost | Low | Higher |
| Toggle cost | Higher | Low |
| Use case | Rare changes | Frequent toggling (e.g., alerts) |
The Vue docs give the same advice I do: v-show for things toggled very often, like alerts or tabs, and v-if when the condition rarely changes or the content is heavy and not worth building in advance.
Practical Example
The mission panel combines two chains of conditions: a system filter and the data loading state. The first part of the template holds the filter buttons:
1<template>
2 <div class="mission-dashboard" style="background: #0a0e27; padding: 1rem;">
3 <div class="system-filter">
4 <button @click="filter = 'all'" class="btn-filter">All</button>
5 <button @click="filter = 'life-support'" class="btn-filter">Life Support</button>
6 <button @click="filter = 'power'" class="btn-filter">Power</button>
7 </div>
8
9 <div v-if="filter === 'all'" class="systems-grid">
10 All systems...
11 </div>
12 <div v-else-if="filter === 'life-support'" class="systems-grid">
13 Life support systems only...
14 </div>
15 <div v-else class="systems-grid">
16 Power systems only...
17 </div>Each button assigns a new value to filter, and the chain shows exactly one grid. The second part of the same template handles loading the sensor data:
1 <!-- Loading state -->
2 <div v-if="isLoading" class="loader" style="color: #00b4d8;">
3 Loading sensor data...
4 </div>
5 <div v-else-if="error" class="error" style="color: #ff4444;">
6 {{ error }}
7 </div>
8 <div v-else class="content" style="color: #00ff88;">
9 {{ data }}
10 </div>
11 </div>
12</template>The order matters: loading first, then the error, and the data last. Four refs drive the whole thing:
1<script setup>
2import { ref } from 'vue'
3
4const filter = ref('all')
5const isLoading = ref(false)
6const error = ref(null)
7const data = ref('All systems nominal')
8</script>Set isLoading.value = true, and the loading message appears instead of the data. In the next lesson you will see v-for and learn why it is not combined with v-if on the same element.
Remember: v-if opens and closes the module hatch, so the element disappears from the DOM, while v-show only draws a curtain behind which the element keeps waiting.
Code for this lesson: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const oxygenLevel = ref(95)
5const temperature = ref(-55)
6const solarStorm = ref(false)
7const crewStatus = ref('all-safe')
8const showWarnings = ref(true)
9
10const alertType = computed(() => {
11 if (oxygenLevel.value < 50) return 'critical'
12 if (oxygenLevel.value < 80) return 'warning'
13 return 'safe'
14})
15
16function simulateStorm() {
17 solarStorm.value = true
18 oxygenLevel.value = 45
19 temperature.value = -80
20 crewStatus.value = 'emergency'
21}
22
23function normalizeConditions() {
24 solarStorm.value = false
25 oxygenLevel.value = 95
26 temperature.value = -55
27 crewStatus.value = 'all-safe'
28}
29
30function adjustOxygen(value) {
31 oxygenLevel.value = Math.max(0, Math.min(100, oxygenLevel.value + value))
32}
33</script>
34
35<template>
36 <div class="conditions-monitor">
37 <h1>Conditions Monitor - Mars Base NOVA</h1>
38
39 <!-- v-if, v-else-if, v-else -->
40 <div class="alert-section">
41 <div v-if="alertType === 'critical'" class="alert alert-critical">
42 CRITICAL ALARM! Oxygen level below safe level!
43 </div>
44 <div v-else-if="alertType === 'warning'" class="alert alert-warning">
45 WARNING: Oxygen level has dropped below normal.
46 </div>
47 <div v-else class="alert alert-safe">
48 All systems operating normally.
49 </div>
50 </div>
51
52 <!-- v-show vs v-if -->
53 <div class="info-panel">
54 <h2>Difference between v-if and v-show</h2>
55 <button @click="showWarnings = !showWarnings" class="btn btn-toggle">
56 Toggle Warnings (v-show)
57 </button>
58
59 <!-- v-show: element stays in DOM, only hidden via CSS -->
60 <div v-show="showWarnings" class="warning-box">
61 <p>This warning is controlled by v-show</p>
62 <p>Element stays in DOM, but is hidden via display: none</p>
63 </div>
64
65 <!-- v-if: element is removed/added from DOM -->
66 <div v-if="showWarnings" class="warning-box">
67 <p>This warning is controlled by v-if</p>
68 <p>Element is completely removed/added from DOM</p>
69 </div>
70 </div>
71
72 <div class="status-grid">
73 <!-- Conditional rendering of the storm status -->
74 <div class="status-card" :class="{ 'storm-active': solarStorm }">
75 <h3>Solar Storm</h3>
76 <div v-if="solarStorm" class="storm-warning">
77 <p class="storm-icon">!!!</p>
78 <p>ACTIVE SOLAR STORM</p>
79 <p>All external operations suspended</p>
80 </div>
81 <div v-else class="storm-calm">
82 <p class="calm-icon">OK</p>
83 <p>Stable conditions</p>
84 </div>
85 </div>
86
87 <!-- Crew status with multiple conditions -->
88 <div class="status-card">
89 <h3>Crew Status</h3>
90 <div v-if="crewStatus === 'emergency'" class="crew-emergency">
91 EMERGENCY - Everyone in the shelter
92 </div>
93 <div v-else-if="crewStatus === 'alert'" class="crew-alert">
94 Elevated Alert State
95 </div>
96 <div v-else-if="crewStatus === 'all-safe'" class="crew-safe">
97 All crew safe - Routine operations
98 </div>
99 <div v-else class="crew-unknown">
100 Status unknown
101 </div>
102 </div>
103
104 <!-- Oxygen level control -->
105 <div class="status-card">
106 <h3>Oxygen Level</h3>
107 <div class="oxygen-display" :class="'oxygen-' + alertType">
108 {{ oxygenLevel }}%
109 </div>
110 <div class="oxygen-controls">
111 <button @click="adjustOxygen(-10)" class="btn btn-small">-10%</button>
112 <button @click="adjustOxygen(10)" class="btn btn-small">+10%</button>
113 </div>
114 </div>
115
116 <!-- Temperature -->
117 <div class="status-card">
118 <h3>Temperature</h3>
119 <div class="temp-display">{{ temperature }}ยฐC</div>
120 <div v-if="temperature < -70" class="temp-warning">
121 Extremely low temperatures
122 </div>
123 </div>
124 </div>
125
126 <div class="controls-section">
127 <h2>Conditions Simulation</h2>
128 <div class="button-group">
129 <button @click="simulateStorm" class="btn btn-danger">
130 Simulate Solar Storm
131 </button>
132 <button @click="normalizeConditions" class="btn btn-success">
133 Restore Normal Conditions
134 </button>
135 </div>
136 </div>
137
138 <div class="info-box">
139 <h3>Conditional Rendering in Vue</h3>
140 <ul>
141 <li><strong>v-if</strong> - Renders the element only when condition is true (removes from DOM)</li>
142 <li><strong>v-else-if</strong> - Additional conditions in the chain</li>
143 <li><strong>v-else</strong> - Default case</li>
144 <li><strong>v-show</strong> - Always in DOM, hides via CSS (better for frequent changes)</li>
145 </ul>
146 </div>
147 </div>
148</template>
149
150<style scoped>
151.conditions-monitor {
152 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
153 min-height: 100vh;
154 padding: 2rem;
155 color: #ffffff;
156 font-family: 'Courier New', monospace;
157}
158
159h1 {
160 text-align: center;
161 color: #00ff88;
162 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
163 margin-bottom: 2rem;
164}
165
166.alert-section {
167 margin-bottom: 2rem;
168}
169
170.alert {
171 padding: 1.5rem;
172 border-radius: 10px;
173 font-size: 1.2rem;
174 font-weight: bold;
175 text-align: center;
176 animation: slideIn 0.5s ease;
177}
178
179@keyframes slideIn {
180 from { transform: translateY(-20px); opacity: 0; }
181 to { transform: translateY(0); opacity: 1; }
182}
183
184.alert-critical {
185 background: rgba(255, 0, 85, 0.2);
186 border: 3px solid #ff0055;
187 animation: pulse 1s infinite;
188}
189
190.alert-warning {
191 background: rgba(255, 180, 0, 0.2);
192 border: 3px solid #ffb400;
193}
194
195.alert-safe {
196 background: rgba(0, 255, 136, 0.2);
197 border: 3px solid #00ff88;
198}
199
200.info-panel {
201 background: rgba(0, 180, 216, 0.1);
202 border: 2px solid #00b4d8;
203 border-radius: 10px;
204 padding: 1.5rem;
205 margin-bottom: 2rem;
206}
207
208.warning-box {
209 background: rgba(255, 180, 0, 0.1);
210 border: 2px solid #ffb400;
211 border-radius: 5px;
212 padding: 1rem;
213 margin-top: 1rem;
214}
215
216.status-grid {
217 display: grid;
218 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
219 gap: 1.5rem;
220 margin-bottom: 2rem;
221}
222
223.status-card {
224 background: rgba(0, 255, 136, 0.1);
225 border: 2px solid #00ff88;
226 border-radius: 10px;
227 padding: 1.5rem;
228 transition: all 0.3s ease;
229}
230
231.status-card.storm-active {
232 border-color: #ff0055;
233 background: rgba(255, 0, 85, 0.2);
234 animation: shake 0.5s infinite;
235}
236
237@keyframes shake {
238 0%, 100% { transform: translateX(0); }
239 25% { transform: translateX(-5px); }
240 75% { transform: translateX(5px); }
241}
242
243.status-card h3 {
244 color: #00b4d8;
245 margin-top: 0;
246}
247
248.storm-warning, .storm-calm {
249 text-align: center;
250}
251
252.storm-icon {
253 font-size: 3rem;
254 margin: 0;
255 animation: flicker 0.3s infinite;
256}
257
258@keyframes flicker {
259 0%, 100% { opacity: 1; }
260 50% { opacity: 0.5; }
261}
262
263.calm-icon {
264 font-size: 3rem;
265 margin: 0;
266}
267
268.crew-emergency {
269 color: #ff0055;
270 font-weight: bold;
271 padding: 1rem;
272 background: rgba(255, 0, 85, 0.2);
273 border-radius: 5px;
274}
275
276.crew-alert {
277 color: #ffb400;
278 font-weight: bold;
279}
280
281.crew-safe {
282 color: #00ff88;
283 font-weight: bold;
284}
285
286.oxygen-display {
287 font-size: 3rem;
288 text-align: center;
289 font-weight: bold;
290 margin: 1rem 0;
291}
292
293.oxygen-critical {
294 color: #ff0055;
295 animation: pulse 1s infinite;
296}
297
298.oxygen-warning {
299 color: #ffb400;
300}
301
302.oxygen-safe {
303 color: #00ff88;
304}
305
306.oxygen-controls {
307 display: flex;
308 gap: 1rem;
309 justify-content: center;
310}
311
312.temp-display {
313 font-size: 2.5rem;
314 text-align: center;
315 color: #00b4d8;
316 margin: 1rem 0;
317}
318
319.temp-warning {
320 color: #00b4d8;
321 text-align: center;
322 font-size: 0.9rem;
323}
324
325.controls-section {
326 background: rgba(255, 255, 255, 0.05);
327 border: 2px solid #00ff88;
328 border-radius: 10px;
329 padding: 1.5rem;
330 margin-bottom: 2rem;
331}
332
333.button-group {
334 display: flex;
335 gap: 1rem;
336 justify-content: center;
337 flex-wrap: wrap;
338}
339
340.btn {
341 padding: 1rem 2rem;
342 border: none;
343 border-radius: 5px;
344 font-size: 1rem;
345 font-weight: bold;
346 cursor: pointer;
347 transition: all 0.3s ease;
348 font-family: 'Courier New', monospace;
349}
350
351.btn-toggle {
352 background: #00b4d8;
353 color: #ffffff;
354}
355
356.btn-danger {
357 background: #ff0055;
358 color: #ffffff;
359}
360
361.btn-success {
362 background: #00ff88;
363 color: #0a0e27;
364}
365
366.btn-small {
367 padding: 0.5rem 1rem;
368 font-size: 0.9rem;
369 background: #00b4d8;
370 color: #ffffff;
371}
372
373.btn:hover {
374 transform: scale(1.05);
375 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
376}
377
378.info-box {
379 background: rgba(0, 255, 136, 0.05);
380 border: 2px solid #00ff88;
381 border-radius: 10px;
382 padding: 1.5rem;
383}
384
385.info-box h3 {
386 color: #00ff88;
387 margin-top: 0;
388}
389
390.info-box ul {
391 list-style: none;
392 padding: 0;
393}
394
395.info-box li {
396 padding: 0.5rem 0;
397 color: #ffffff;
398 border-left: 3px solid #00b4d8;
399 padding-left: 1rem;
400 margin: 0.5rem 0;
401}
402
403.info-box strong {
404 color: #00b4d8;
405}
406</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the difference between v-if and v-show?
2. When is it better to use v-show instead of v-if?
3. Can you use v-else-if in Vue?
4. Which code snippet correctly uses v-if with v-else?
5. What happens to a DOM element when the v-if condition is set to false?
Hands-on tasks in the game
- Code editor
Use v-if, v-else-if, and v-else for conditional rendering of system statuses.
- Vertical ordering
Arrange the correct conditional chain from v-if to v-else:
- Vertical ordering
Order the steps of v-if from condition check to rendering:
- Click in order
Arrange the syntax: first the toggle button, then the element with v-show below it:
- Code editor
Use v-show with a toggle button to hide/show sections.