---
title: "v-if, v-show and v-for: the directives you use every day · vuejs.school"
description: "v-if removes an element, v-show hides it and v-for repeats it. Here is when each is cheaper, why index keys cause bugs, and the two mistakes to avoid."
canonical_url: "https://vuejs.school/community/blog/v-if-v-show-and-v-for"
meta:
  "og:description": "v-if removes an element, v-show hides it and v-for repeats it. Here is when each is cheaper, why index keys cause bugs, and the two mistakes to avoid."
  "og:image": "https://vuejs.school/_og/d/a_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXk%2Cdescription_~di1pZiByZW1vdmVzIGFuIGVsZW1lbnQsIHYtc2hvdyBoaWRlcyBpdCBhbmQgdi1mb3IgcmVwZWF0cyBpdC4gSGVyZSBpcyB3aGVuIGVhY2ggaXMgY2hlYXBlciwgd2h5IGluZGV4IGtleXMgY2F1c2UgYnVncywgYW5kIHRoZSB0d28gbWlzdGFrZXMgdG8gYXZvaWQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92LWlmLXYtc2hvdy1hbmQtdi1mb3Ii%2Cs_wjFamXbQPcU1dPih.png"
  "og:image:alt": "v-if, v-show and v-for: the directives you use every day · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "v-if, v-show and v-for: the directives you use every day · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXk%2Cdescription_~di1pZiByZW1vdmVzIGFuIGVsZW1lbnQsIHYtc2hvdyBoaWRlcyBpdCBhbmQgdi1mb3IgcmVwZWF0cyBpdC4gSGVyZSBpcyB3aGVuIGVhY2ggaXMgY2hlYXBlciwgd2h5IGluZGV4IGtleXMgY2F1c2UgYnVncywgYW5kIHRoZSB0d28gbWlzdGFrZXMgdG8gYXZvaWQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92LWlmLXYtc2hvdy1hbmQtdi1mb3Ii%2Cs_wjFamXbQPcU1dPih.png"
  "twitter:image:alt": "v-if, v-show and v-for: the directives you use every day · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## v-if removes, v-show hides

Both directives take an expression and react to it. The difference is what they do to the DOM, the tree of elements the browser builds from your HTML.

*src/components/Details.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <button type="button" @click="open = !open">Details</button>
  <p v-if="open">I exist only while open is true.</p>
  <p v-show="open">I am always in the DOM, hidden with display: none.</p>
</template>
```

With `open` set to `false`, the first paragraph is not in the page at all. Vue leaves a comment node as a placeholder (`<!--v-if-->` in development) and creates the real element when the condition becomes true. The second paragraph is created once, at the start, and Vue toggles the inline style `display: none` on it. Open your browser's element inspector and click the button a few times: one paragraph appears and disappears from the tree, the other only changes its `style` attribute.

Two limits follow from that. `v-show` cannot be used on a `<template>` element, because a `<template>` has no element of its own to hide. And `v-show` has no `v-else`; if you need a second branch, you need `v-if`.

## Use v-show for frequent toggles, v-if for the rest

`v-if` is lazy. If the condition starts false, Vue does no work for that branch: no child components are created, no `onMounted` hooks run, no data is fetched. The cost arrives when the condition flips, because every toggle destroys the subtree and builds it again from nothing. `v-show` is the reverse: it pays the full rendering cost up front, once, and each toggle afterwards costs one style change.

So choose by how often the thing toggles. A dropdown menu, a tooltip, the panels of a tab bar: these open and close again and again, and `v-show` keeps them instant. A settings form, a chart that loads a library, content only an administrator may see: these are shown rarely or never, and `v-if` means the user who never opens them never pays for them.

`v-if` is also the one to use when the element must not be in the HTML at all, because an element hidden with `v-show` is still in the DOM and in any HTML rendered on the server. That is not protection, though: the template is compiled into the JavaScript the browser downloads, so data meant only for administrators should come from a server that checks who is asking.

## Add branches with v-else-if and v-else

A `v-else` or `v-else-if` element must come straight after the `v-if` or `v-else-if` element, with no other element in between, or Vue reports a compile error. A loading state is the usual shape (the fetch that sets `status` and `orders` is left out):

*src/components/Orders.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

type Status = 'loading' | 'error' | 'ready'

const status = ref<Status>('loading')
const orders = ref<string[]>([])
</script>

<template>
  <p v-if="status === 'loading'">Loading your orders…</p>
  <p v-else-if="status === 'error'">We could not load your orders.</p>
  <p v-else-if="orders.length === 0">No orders yet.</p>
  <ul v-else>
    <li v-for="order in orders" :key="order">{{ order }}</li>
  </ul>
</template>
```

When one branch needs several elements, wrap them in `<template v-if="...">`. The `<template>` tag renders nothing itself, so you get a group without an extra `<div>` in the page.

## Loop over arrays, objects and ranges with v-for

`v-for` takes the form `item in items` and renders its element once per item. A second name gives you the position: `(item, index) in items`. You can write `of` instead of `in`; they mean the same thing. The `:key` attribute tells Vue which rendered element belongs to which item, and every `v-for` should have one. In this to-do list, each row's key is the todo's `id`:

*src/components/TodoList.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

type Todo = { id: number; text: string; done: boolean }

const todos = ref<Todo[]>([
  { id: 1, text: 'Read about directives', done: true },
  { id: 2, text: 'Build a todo list', done: false },
])
</script>

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <label>
        <input v-model="todo.done" type="checkbox" />
        {{ todo.text }}
      </label>
    </li>
  </ul>
</template>
```

Objects work too, with the value first and the key second: `(value, key) in settings`. A plain number renders a range, and the count starts at one, not zero. Here `settings` is an object such as `{ theme: 'dark', fontSize: 16 }`, and a `<template>` groups each `<dt>` with its `<dd>`:

```
<dl>
  <template v-for="(value, key) in settings" :key="key">
    <dt>{{ key }}</dt>
    <dd>{{ value }}</dd>
  </template>
</dl>

<button v-for="page in 5" :key="page" type="button">{{ page }}</button>
```

A `:key` must be a string or a number, and it must be unique among its siblings. It should also be stable: the same item must get the same key every render. The `id` from your database is the normal choice.

## Why index keys cause bugs

The tempting shortcut is `:key="index"`. It satisfies the rule that every item has a key, and it works until items change position. Here is a guest list where each row has a text box the user types into; `guests` is a `ref` holding objects with an `id` and a `name`:

*src/components/Guests.vue**Breaks***

```
<li v-for="(guest, index) in guests" :key="index">
  {{ guest.name }}
  <input type="text" placeholder="Dietary notes" />
  <button type="button" @click="guests.splice(index, 1)">Remove</button>
</li>
```

Start with three guests, type a note next to the first, then remove that guest. The note stays in the first row, now next to the second guest's name. Here is what Vue did:

1. Before the removal the rows had keys `0`, `1` and `2`. After it, two guests remain, with keys `0` and `1`.
2. Vue matches rows by key, so it kept the `<li>` elements keyed `0` and `1`, updated the name inside each, and removed the one keyed `2`.
3. The `<input>` is not bound to any data, so its typed value belongs to the DOM element, and that element stayed where it was.

The same thing happens to a child component's own state and to enter and leave transitions. The fix changes only the key:

*src/components/Guests.vue**Works***

```
<li v-for="(guest, index) in guests" :key="guest.id">
  {{ guest.name }}
  <input type="text" placeholder="Dietary notes" />
  <button type="button" @click="guests.splice(index, 1)">Remove</button>
</li>
```

With `guest.id` as the key, Vue removes the `<li>` that belonged to the removed guest, text box and all, and leaves the others alone. An index is acceptable only for a list that is never reordered, filtered or spliced, and most lists eventually are.

## Keep v-if and v-for on separate elements

Vue 3 evaluates `v-if` before `v-for` when both sit on one element. The condition therefore runs outside the loop and cannot see the loop variable:

*src/components/TodoList.vue**Breaks***

```
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
  {{ todo.text }}
</li>
```

In development the console shows `[Vue warn]: Property "todo" was accessed during render but is not defined on instance.`, and then the render throws a `TypeError`, because `todo` is `undefined` when the condition runs. The lint rule `vue/no-use-v-if-with-v-for` in eslint-plugin-vue catches this before you run anything.

The fix depends on what you meant. To filter the list, do it in a computed property, which also keeps the template readable:

*src/components/TodoList.vue**Works***

```
<script setup lang="ts">
import { computed, ref } from 'vue'

type Todo = { id: number; text: string; done: boolean }

const todos = ref<Todo[]>([])
const openTodos = computed(() => todos.value.filter(todo => !todo.done))
</script>

<template>
  <ul>
    <li v-for="todo in openTodos" :key="todo.id">{{ todo.text }}</li>
  </ul>
</template>
```

To skip or show the whole list, put the `v-if` on the wrapping `<ul>`. If you really need a per-item condition in the template, use `<template v-for="todo in todos" :key="todo.id">` with `<li v-if="!todo.done">` inside it. In Vue 3 the key goes on the `<template>`, not on the inner element.

## Write v-bind and v-on as : and @

Two more directives are so common that they have one-character forms, and most code uses them, this post included:

- `v-bind:src="url"` binds an attribute to an expression and is written `:src="url"`.
- `v-on:click="save"` listens for an event and is written `@click="save"`.

Two details about `v-bind` are worth knowing early. When the variable has the same name as the attribute, Vue 3.4 and later let you write `:id` on its own instead of `:id="id"`. And `v-bind="props"` with no attribute name spreads every property of an object onto the element, which is how wrapper components pass attributes through.

## Skip updates with v-once and v-memo

`v-once` renders an element and its children one time and never updates them again, which suits content that is built from props at creation and will not change. `v-memo="[selectedId === item.id]"` tells Vue to skip re-rendering a subtree unless one of the values in the array changed, and it is a tool for very long `v-for` lists where most rows are unaffected by an update. Reach for neither until a profiler tells you to.

**Check it stuck**3 questions from the Directives quiz.

[**Practice now**](https://vuejs.school/learn/beginner/directives?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 5 min read**Forms and inputs: v-model modifiers, checkboxes and selects**](https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects) [Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/v-if-v-show-and-v-for#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-01-29","description":"v-if removes an element, v-show hides it and v-for repeats it. Here is when each is cheaper, why index keys cause bugs, and the two mistakes to avoid.","name":"v-if, v-show and v-for: the directives you use every day","url":"https://vuejs.school/community/blog/v-if-v-show-and-v-for","breadcrumb":{"@id":"https://vuejs.school/community/blog/v-if-v-show-and-v-for#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/v-if-v-show-and-v-for"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/v-if-v-show-and-v-for#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-01-29","description":"v-if removes an element, v-show hides it and v-for repeats it. Here is when each is cheaper, why index keys cause bugs, and the two mistakes to avoid.","headline":"v-if, v-show and v-for: the directives you use every day","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXk%2Cdescription_~di1pZiByZW1vdmVzIGFuIGVsZW1lbnQsIHYtc2hvdyBoaWRlcyBpdCBhbmQgdi1mb3IgcmVwZWF0cyBpdC4gSGVyZSBpcyB3aGVuIGVhY2ggaXMgY2hlYXBlciwgd2h5IGluZGV4IGtleXMgY2F1c2UgYnVncywgYW5kIHRoZSB0d28gbWlzdGFrZXMgdG8gYXZvaWQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92LWlmLXYtc2hvdy1hbmQtdi1mb3Ii%2Cs_wjFamXbQPcU1dPih.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/v-if-v-show-and-v-for#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/v-if-v-show-and-v-for#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/v-if-v-show-and-v-for#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"v-if, v-show and v-for: the directives you use every day","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXk%2Cdescription_~di1pZiByZW1vdmVzIGFuIGVsZW1lbnQsIHYtc2hvdyBoaWRlcyBpdCBhbmQgdi1mb3IgcmVwZWF0cyBpdC4gSGVyZSBpcyB3aGVuIGVhY2ggaXMgY2hlYXBlciwgd2h5IGluZGV4IGtleXMgY2F1c2UgYnVncywgYW5kIHRoZSB0d28gbWlzdGFrZXMgdG8gYXZvaWQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92LWlmLXYtc2hvdy1hbmQtdi1mb3Ii%2Cs_wjFamXbQPcU1dPih.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~di1pZiwgdi1zaG93IGFuZCB2LWZvcjogdGhlIGRpcmVjdGl2ZXMgeW91IHVzZSBldmVyeSBkYXk%2Cdescription_~di1pZiByZW1vdmVzIGFuIGVsZW1lbnQsIHYtc2hvdyBoaWRlcyBpdCBhbmQgdi1mb3IgcmVwZWF0cyBpdC4gSGVyZSBpcyB3aGVuIGVhY2ggaXMgY2hlYXBlciwgd2h5IGluZGV4IGtleXMgY2F1c2UgYnVncywgYW5kIHRoZSB0d28gbWlzdGFrZXMgdG8gYXZvaWQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92LWlmLXYtc2hvdy1hbmQtdi1mb3Ii%2Cs_wjFamXbQPcU1dPih.png"}]}
```