Vue.js 7 🟢 Conditional Rendering Mechanics with v-if, v-else-if, and v-else
Not every part of a user interface should appear all the time. A loading spinner shows only while data fetches. An error message appears only after something fails. An admin panel renders only for users with the right permissions. Vue handles all of these cases through its conditional rendering system, a set of directives that add or remove content from the DOM based on the truthiness of expressions.
This chapter covers the three directives that form Vue’s conditional rendering toolkit: v-if, v-else-if, and v-else. You will learn how each directive works, how they chain together to form branching logic, and how to use the <template> element as an invisible wrapper when you need to toggle multiple elements at once. You will also encounter a subtle but important behavior: Vue’s element reuse strategy and how the key attribute gives you control over it.
By the end, you will understand not just the syntax but the mechanics underneath—why v-if is considered “real” conditional rendering, how Vue’s diffing algorithm decides whether to reuse or recreate elements, and when the interplay between these directives can produce surprising results.
Key point: Conditional rendering in Vue is not about hiding elements with CSS. v-if and its companions actually add and remove elements from the DOM, which means child components are destroyed and recreated, event listeners are torn down and rebuilt, and state inside those components is reset each time the condition changes.
Why v-if exists
The static template problem. A Vue template without conditionals renders the same structure every time. But real interfaces are not static. Form validation messages appear only when fields are invalid. Navigation items change based on authentication state. Content sections load asynchronously. Without conditional rendering, developers would need to manipulate the DOM directly—querying elements, appending or removing nodes, and manually cleaning up event listeners. Vue’s directives solve this declaratively: you describe when content should exist, and Vue handles the DOM operations.
The difference between hiding and removing. A common mistake is to think of conditional rendering as visibility control. It is not. CSS has display: none for hiding elements while keeping them in the DOM. v-if does something different: it removes the element entirely. The distinction matters because removed elements lose their state, their event listeners, and their connection to the component tree . When the condition becomes true again, Vue builds the element from scratch. This is more expensive than toggling CSS, but it also means the element does not exist when it should not—which can matter for performance, memory, and semantics.
The branching need. A simple toggle between “show” and “hide” covers many cases, but real applications need more than binary choices. A form might show different fields depending on the selected account type. A dashboard might render different widgets based on the user’s role. v-else-if and v-else extend the basic conditional into a full branching structure, mirroring the if / else if / else pattern familiar from JavaScript . This chaining makes complex conditional logic expressible directly in the template.
The multi-element problem. A directive, by definition, attaches to a single element. But sometimes you need to toggle an entire section containing a heading, several paragraphs, and a button. You could wrap them in a <div>, but that adds an extra DOM node with no semantic meaning. Vue’s <template> element solves this: it serves as an invisible wrapper that does not appear in the rendered output . This keeps the DOM clean while still allowing group-level conditional rendering.
The element reuse optimization. Vue’s rendering engine tries to be efficient. When it updates the DOM, it compares the new virtual tree against the old one and makes the minimum number of changes. If two elements in different branches look similar—say, both are <input> elements—Vue might reuse the same DOM node rather than destroying and recreating it. This is usually good for performance, but it can cause unexpected behavior when the elements have different purposes. The key attribute gives developers a way to override this optimization and force Vue to treat elements as distinct .
a. v-if and v-else
The v-if directive evaluates an expression and renders its host element only when that expression is truthy. A companion directive, v-else, provides the fallback when the condition is falsy.
<template>
<h1 v-if="isLoggedIn">Welcome back!</h1>
<h1 v-else>Please log in.</h1>
</template>
The truthiness rules follow JavaScript: false, 0, "", null, undefined, and NaN are all falsy. Everything else is truthy. A v-else element must immediately follow a v-if or v-else-if element. If any other element sits between them, Vue will not recognize the connection and the v-else will be ignored .
The expression inside v-if can be anything: a boolean ref, a comparison, a computed property, or a method call. Vue re-evaluates it whenever the reactive dependencies change and re-renders accordingly.
<div v-if="items.length > 0">You have {{ items.length }} items.</div>
<div v-else>Your cart is empty.</div>
When the condition is false, the element is completely removed from the DOM. There is no hidden element, no residual event listener, no preserved component state. This is the defining characteristic of v-if.
b. v-else-if and chained conditionals
When a simple binary choice is not enough, v-else-if adds additional branches. It must immediately follow a v-if or another v-else-if, and the chain can extend as long as needed.
<div v-if="status === 'loading'">Loading...</div>
<div v-else-if="status === 'error'">Something went wrong.</div>
<div v-else-if="status === 'empty'">No results found.</div>
<div v-else>Here are your results.</div>
Vue evaluates these conditions in order and renders the first branch whose expression is truthy. Once a match is found, the remaining branches are skipped, just like a JavaScript if / else if / else statement .
The chaining requirement is strict. A v-else-if element must be the immediate sibling following a v-if or v-else-if. Comments are allowed between them, but any actual element breaks the chain . This is a common source of confusion when developers add whitespace, wrapping elements, or formatting that separates the branches.
A practical pattern is to use v-else-if for state machines: loading, success, error, empty. Each state maps to a distinct template fragment, and the chain ensures only one renders at a time.
c. v-if on <template> and the key attribute
When you need to toggle more than one element, attaching v-if to a single root will not work. The <template> element solves this by acting as an invisible container.
<template v-if="user.isAdmin">
<h2>Admin Controls</h2>
<button @click="deleteAll">Delete Everything</button>
<p>These actions are irreversible.</p>
</template>
The <template> tag does not appear in the rendered DOM. Only its contents are inserted or removed as a group . This keeps the markup semantically clean while still enabling group-level conditional rendering.
A more subtle issue arises when Vue’s element reuse optimization intersects with conditional branches. Consider a login form that switches between username and email modes:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email">
</template>
Switching loginType does not clear what the user has typed. Vue sees that both branches render an <input> element and reuses the same DOM node, only updating the placeholder . This is usually efficient, but sometimes undesirable—the username input and email input should be separate.
The key attribute forces Vue to treat elements as distinct:
<input placeholder="Enter your username" key="username-input">
<input placeholder="Enter your email" key="email-input">
Now switching the mode destroys the old input and creates a new one, clearing any typed value . The <label> elements, lacking keys, are still reused efficiently. This pattern is essential whenever conditional branches contain elements that should maintain independent state.
Complete Example Session
<!-- ============================================ -->
<!-- PART 1: BASIC v-if AND v-else -->
<!-- ============================================ -->
<template>
<h1 v-if="isVisible">Now you see me</h1>
<h1 v-else>Now you don't</h1>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
<!-- ============================================ -->
<!-- PART 2: v-else-if CHAIN -->
<!-- ============================================ -->
<template>
<div v-if="status === 'loading'">Loading...</div>
<div v-else-if="status === 'success'">Data loaded.</div>
<div v-else-if="status === 'error'">Error occurred.</div>
<div v-else>Unknown state.</div>
</template>
<script setup>
import { ref } from 'vue'
const status = ref('loading')
</script>
<!-- ============================================ -->
<!-- PART 3: v-if ON TEMPLATE -->
<!-- ============================================ -->
<template>
<template v-if="isAuthorized">
<h2>Admin Panel</h2>
<p>Restricted content</p>
<button>Dangerous Action</button>
</template>
<p v-else>You do not have access.</p>
</template>
<script setup>
import { ref } from 'vue'
const isAuthorized = ref(false)
</script>
<!-- ============================================ -->
<!-- PART 4: ELEMENT REUSE DEMONSTRATION -->
<!-- ============================================ -->
<template>
<template v-if="mode === 'username'">
<label>Username</label>
<input placeholder="Enter username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter email">
</template>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('username')
// Typing in the input then switching mode preserves the value
</script>
<!-- ============================================ -->
<!-- PART 5: FORCING RECREATION WITH key -->
<!-- ============================================ -->
<template>
<template v-if="mode === 'username'">
<label>Username</label>
<input placeholder="Enter username" key="username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter email" key="email">
</template>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('username')
// Switching now clears the input because Vue recreates it
</script>
<!-- ============================================ -->
<!-- PART 6: COMPUTED CONDITION -->
<!-- ============================================ -->
<template>
<div v-if="showError">Error: {{ errorMessage }}</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const error = ref(null)
const showError = computed(() => error.value !== null)
const errorMessage = computed(() => error.value?.message ?? '')
</script>
<!-- ============================================ -->
<!-- PART 7: TOGGLE PATTERN -->
<!-- ============================================ -->
<template>
<button @click="isOpen = !isOpen">Toggle</button>
<div v-if="isOpen">Content is visible</div>
</template>
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
</script>
<!-- ============================================ -->
<!-- PART 8: MULTIPLE CONDITIONS WITH AND -->
<!-- ============================================ -->
<template>
<button v-if="isAdmin && hasPermission && !isLocked">
Delete
</button>
</template>
<script setup>
import { ref } from 'vue'
const isAdmin = ref(true)
const hasPermission = ref(false)
const isLocked = ref(false)
</script>
<!-- ============================================ -->
<!-- PART 9: CONDITIONAL WITH COMPONENT -->
<!-- ============================================ -->
<template>
<UserProfile v-if="user" :user="user" />
<LoginPrompt v-else />
</template>
<script setup>
import { ref } from 'vue'
import UserProfile from './UserProfile.vue'
import LoginPrompt from './LoginPrompt.vue'
const user = ref(null)
</script>
<!-- ============================================ -->
<!-- PART 10: BRANCHING WITH FALLBACK -->
<!-- ============================================ -->
<template>
<div v-if="loadState === 'idle'">Ready to load</div>
<div v-else-if="loadState === 'loading'">Loading...</div>
<div v-else-if="loadState === 'error'">Failed: {{ error }}</div>
<div v-else-if="loadState === 'empty'">No results</div>
<div v-else>{{ results }}</div>
</template>
<script setup>
import { ref } from 'vue'
const loadState = ref('idle')
const error = ref('')
const results = ref([])
</script>
The ten parts covered the essential mechanics: basic v-if and v-else, chained v-else-if, <template> grouping, element reuse and the key override, computed conditions, toggle patterns, boolean composition, component conditionals, and a full state machine branch.
Quick Reference
Core Directives
| Directive | Expression | Behavior |
|---|---|---|
v-if | Any expression | Renders when truthy |
v-else-if | Any expression | Renders when truthy and previous branches falsy |
v-else | None | Renders when all previous branches falsy |
Chain Rules
| Rule | Consequence of Violation |
|---|---|
v-else must immediately follow v-if or v-else-if | Directive is ignored |
v-else-if must immediately follow v-if or v-else-if | Directive is ignored |
| No elements between branches | Chain breaks |
Element Behavior
| Aspect | Without key | With key |
|---|---|---|
| Similar elements in branches | Reused | Recreated |
| Input value on toggle | Preserved | Cleared |
| Component state | Preserved | Reset |
v-if vs v-show
| Aspect | v-if | v-show |
|---|---|---|
| DOM presence | Removed when false | Always present |
| Toggle cost | Higher (destroy/recreate) | Lower (CSS toggle) |
| Initial cost | Lower (lazy) | Higher (always renders) |
<template> support | Yes | No |
v-else support | Yes | No (problematic) |
Best Practices
✅ Do This:
<!-- Use v-else-if for multiple branches -->
<div v-if="a">A</div>
<div v-else-if="b">B</div>
<div v-else>C</div> <!-- ✅ -->
<!-- Use template for multiple elements -->
<template v-if="show"> <!-- ✅ -->
<h1>Title</h1>
<p>Body</p>
</template>
<!-- Use key when inputs should be independent -->
<input v-if="mode === 'a'" key="a"> <!-- ✅ -->
<input v-else key="b">
<!-- Use computed for complex conditions -->
<div v-if="shouldShow">Content</div> <!-- ✅ -->
<!-- Use v-if for rarely-toggled content -->
<AdminPanel v-if="isAdmin" /> <!-- ✅ -->
<!-- Place v-if on outer element for lists -->
<div v-if="items.length"> <!-- ✅ -->
<Item v-for="item in items" :key="item.id" />
</div>
❌ Don’t Do This:
<!-- Don't separate branches with elements -->
<div v-if="a">A</div>
<span>separator</span> <!-- ❌ -->
<div v-else>B</div>
<!-- Don't use v-if and v-for on same element -->
<li v-for="item in items" v-if="item.active"> <!-- ❌ -->
<!-- Don't forget key when inputs should reset -->
<input v-if="mode === 'a'"> <!-- ❌ (reuses) -->
<input v-else>
<!-- Don't use v-if for frequent toggles -->
<div v-if="isHovering">Tooltip</div> <!-- ❌ (use v-show) -->
<!-- Don't put v-else on non-adjacent element -->
<div v-if="a">A</div>
<div>Other</div>
<div v-else>B</div> <!-- ❌ (ignored) -->
<!-- Don't use v-show with v-else on components -->
<Comp v-show="a" /> <!-- ❌ -->
<p v-else>Fallback</p>
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
v-else not recognized | Non-adjacent element between branches | Ensure v-else immediately follows |
| Input value persists unexpectedly | Vue reuses similar elements | Add unique key to force recreation |
| Component state resets on toggle | v-if destroys and recreates | Use v-show if state should persist |
v-if with v-for on same element | Unclear which has priority | Move v-if to wrapper or use computed filter |
v-show with v-else fails | Directive priority conflict | Use v-if for branching logic |
| Performance degradation | Frequent toggles with v-if | Switch to v-show for frequent toggles |
v-if on <template> not working | Syntax error in template | Ensure <template> is single root |
Real-World Examples
1. Loading State UI
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ data }}</div>
2. Authentication Guard
<AdminDashboard v-if="user.role === 'admin'" />
<UserDashboard v-else />
3. Empty State
<ul v-if="items.length">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
4. Form Field Conditional
<input v-if="needsTaxId" placeholder="Tax ID" key="tax" />
<input v-else placeholder="SSN" key="ssn" />
5. Multi-Step Wizard
<div v-if="step === 1">Step One</div>
<div v-else-if="step === 2">Step Two</div>
<div v-else-if="step === 3">Step Three</div>
6. Permission-Based Button
<button v-if="canEdit" @click="edit">Edit</button>
<span v-else>Read-only</span>
7. Search Results
<div v-if="searching">Searching...</div>
<div v-else-if="results.length === 0">No matches</div>
<div v-else>{{ results.length }} results</div>
8. Modal Content
<template v-if="modalType === 'confirm'">
<ConfirmDialog />
</template>
<template v-else>
<InfoDialog />
</template>
9. Feature Flag
<NewFeature v-if="flags.newFeature" />
<OldFeature v-else />
10. Error Boundary
<ErrorDisplay v-if="hasError" :error="error" />
<Content v-else :data="data" />
Visual
Branch Evaluation Flow
┌─────────────────────────────────────────────────────────────┐
│ v-if / v-else-if / v-else EVALUATION │
│ │
│ Start │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ v-if expression │──truthy──▶ Render this block │
│ └────────┬────────┘ │
│ │ falsy │
│ ▼ │
│ ┌─────────────────┐ │
│ │ v-else-if expr │──truthy──▶ Render this block │
│ └────────┬────────┘ │
│ │ falsy │
│ ▼ │
│ ┌─────────────────┐ │
│ │ v-else │─────────▶ Render this block │
│ └─────────────────┘ │
│ │
│ Only ONE block renders. First truthy wins. │
│ │
└─────────────────────────────────────────────────────────────┘
Element Reuse vs Recreation
┌─────────────────────────────────────────────────────────────┐
│ WITHOUT key: ELEMENT REUSE │
│ │
│ Before toggle After toggle │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ <input> │ │ <input> │ Same DOM node │
│ │ value="John" │──────▶│ value="John" │ Value persists │
│ │ placeholder │ │ placeholder │ │
│ │ ="Username" │ │ ="Email" │ │
│ └──────────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ WITH key: ELEMENT RECREATION │
│ │
│ Before toggle After toggle │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ <input> │ │ <input> │ New DOM node │
│ │ key="user" │──────▶│ key="email" │ Value cleared │
│ │ value="John" │ │ value="" │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
v-if vs v-show: DOM State
┌─────────────────────────────────────────────────────────────┐
│ v-if: CONDITION FALSE │
│ │
│ DOM: (nothing) │
│ Component: destroyed │
│ Event listeners: removed │
│ State: lost │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ v-show: CONDITION FALSE │
│ │
│ DOM: <div style="display: none"> │
│ Component: alive │
│ Event listeners: active │
│ State: preserved │
│ │
└─────────────────────────────────────────────────────────────┘
Chain Structure
┌─────────────────────────────────────────────────────────────┐
│ CONDITIONAL CHAIN STRUCTURE │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <div v-if="type === 'A'"> │ │
│ │ Branch A │ │
│ │ </div> │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ <div v-else-if="type === 'B'"> ← must be adjacent │ │
│ │ Branch B │ │
│ │ </div> │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ <div v-else-if="type === 'C'"> ← must be adjacent │ │
│ │ Branch C │ │
│ │ </div> │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ <div v-else> ← must be adjacent │ │
│ │ Fallback │ │
│ │ </div> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Any gap breaks the chain. v-else and v-else-if ignored. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Core directive | v-if conditionally renders based on truthiness |
| Fallback | v-else renders when v-if is falsy |
| Branching | v-else-if chains additional conditions |
| Grouping | <template v-if> wraps multiple elements invisibly |
| Chain rule | Branches must be immediate siblings |
| DOM behavior | Elements removed, not hidden |
| Component behavior | Destroyed and recreated on toggle |
| Reuse behavior | Vue reuses similar elements unless key differs |
v-show difference | Keeps element in DOM, toggles CSS |
| Toggle cost | v-if higher; v-show lower |
| Initial cost | v-if lower (lazy); v-show higher |
Key takeaways:
v-ifremoves elements from the DOM. Unlike CSS visibility, the element and its children cease to exist when the condition is false. This affects component lifecycle, event listeners, and local state.- The chain must be unbroken. A
v-else-iforv-elseelement must immediately follow its predecessor. Any intervening element, even a comment in some edge cases, breaks the connection and the directive is silently ignored. <template>enables group conditionals. When multiple elements should toggle together, wrap them in a<template>withv-if. The template itself does not render, only its contents.- Element reuse can preserve state unexpectedly. Vue’s optimization may reuse DOM nodes across conditional branches. When this is undesirable, add a unique
keyto force recreation. v-ifis lazy. If the condition is false on initial render, Vue does nothing. The block is only compiled and mounted when the condition first becomes true. This is efficient for content that rarely appears.v-else-ifmirrors JavaScript branching. The evaluation order matchesif / else if / else. The first truthy branch wins, and subsequent conditions are not evaluated.v-ifandv-forshould not share an element. The combination creates unclear intent and unnecessary work. Use a computed property to filter, or wrap thev-ifaround the list.- Use
v-iffor rare toggles,v-showfor frequent ones. The trade-off is between high toggle cost (v-if) and high initial cost (v-show).
Remember: Conditional rendering in Vue is not about visibility. It is about existence. When v-if is false, the element does not exist in the DOM, its component is destroyed, its state is gone. This is both the power and the responsibility of the directive. It gives you precise control over what the user sees and what resources the browser commits. But it also means you must think carefully about what happens when content reappears—whether state should persist, whether elements should be recreated, and whether the cost of that recreation is acceptable. The key attribute is your tool for resolving the ambiguity when Vue’s reuse optimization makes the wrong choice.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!