Conditional and List Rendering
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Two directive families control what a template produces: v-if / v-show decide whether an element
renders, and v-for repeats one. They follow Conditional
Rendering and List Rendering.
v-if, v-else, v-else-if
v-if renders its element only when the expression is truthy. v-else-if and v-else chain from it and
must sit on the immediately following sibling element.
<template>
<p v-if="status === 'loading'">Loading...</p>
<p v-else-if="status === 'error'">Something went wrong.</p>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.label }}</li>
</ul>
</template>
When v-if is false the element and its component children are not created; toggling it later mounts and
unmounts them, running lifecycle hooks each time.
v-if on <template>
To toggle several elements as a unit without adding a wrapper, put v-if on a <template> tag. It renders
its children only, no extra DOM node:
<template>
<template v-if="showDetails">
<h2>Details</h2>
<p>{{ description }}</p>
</template>
</template>
v-else / v-else-if also work on <template>.
v-if vs. v-show
v-show always renders the element and toggles its CSS display property. It has no <template> form and
no v-else partner.
v-if |
v-show |
|
|---|---|---|
Toggle cost |
mounts / unmounts the subtree |
flips one style |
Initial cost |
nothing rendered when false |
always rendered |
Best for |
rarely-changing conditions, or content that should not exist |
frequent toggles (menus, tabs) |
Use v-show when the thing flips often; use v-if when the condition rarely changes or the branch is
expensive and usually absent.
v-for
v-for iterates a data source. Always pair it with a :key.
Arrays — the item, then an optional index:
<template>
<li v-for="(user, index) in users" :key="user.id">
{{ index }}. {{ user.name }}
</li>
</template>
Objects — value, then optional key, then optional index, in Object.keys() order:
<template>
<li v-for="(value, key) in settings" :key="key">{{ key }}: {{ value }}</li>
</template>
A range — an integer n iterates 1..n:
<template>
<span v-for="n in 5" :key="n">{{ n }}</span> <!-- 1 2 3 4 5 -->
</template>
On <template> — repeat a group with no wrapper element:
<template>
<template v-for="item in items" :key="item.id">
<dt>{{ item.term }}</dt>
<dd>{{ item.definition }}</dd>
</template>
</template>
You can iterate an array-returning computed property to render a filtered or sorted view without mutating the source.
The key requirement
Give every v-for a :key bound to a stable, unique identifier for each item — a database id, not the
array index. The key lets Vue match nodes across re-renders so it moves and reuses DOM and component state
correctly instead of patching in place. An index key breaks as soon as the list is reordered, filtered, or
has items inserted, and it corrupts component state and transitions. Put :key on the <template> tag when
v-for is on it.
Array updates: mutate or replace
Vue detects change on a reactive array in both directions:
-
Mutation methods —
push,pop,shift,unshift,splice,sort,reverse— trigger updates in place. -
Replacement —
filter,map,slice,concatreturn a new array; assign it back (list.value = list.value.filter(…)) and Vue re-renders, reusing DOM where keys match.
Both are fine. Index and length writes (arr[99] = x, arr.length = 0) are also tracked in Vue 3 — see
Reactivity Fundamentals for the reactivity caveats. To show a derived order without
touching the original, return a copy from a computed: […list.value].sort(…).
v-for with a component
v-for on a component still needs a :key, and data does not flow into the component automatically — pass
it as props:
<template>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@remove="removeTodo(todo.id)"
/>
</template>
Do not put v-if and v-for on the same element
It is discouraged, and the precedence is a trap: v-if has higher priority than v-for, so the condition
runs before the loop variable exists, and v-if="todo.done" throws or misbehaves.
Fix it by moving one out:
<template>
<!-- filter in a computed, then loop -->
<li v-for="todo in pendingTodos" :key="todo.id">{{ todo.name }}</li>
<!-- or wrap the loop in a conditional <template> -->
<template v-if="todos.length">
<li v-for="todo in todos" :key="todo.id">{{ todo.name }}</li>
</template>
</template>
const pendingTodos = computed(() => todos.value.filter((t) => !t.done))
See also
-
Template Syntax — directive syntax, and binding
class/styleon the rendered elements. -
Computed Properties — the right place to filter and sort a list for
v-for. -
Reactivity Fundamentals — why reactive arrays update, and their edge cases.
-
Web Accessibility — keeping conditionally rendered UI (menus, dialogs, live regions) accessible.