Vue 3 tutorial – list rendering with v-for and :key
verified on 2 September 2026 · 3 min
v-for="item in items" repeats an element for every entry in an array. Always add a unique, stable :key taken from your data rather than from the index, so that Vue tracks each item correctly through updates.
A product page is full of lists: the fabric composition, the colour variants, the reviews. The v-for directive repeats one element of the template for every entry in an array.
Looping over a plain array
Add the composition of the product to the state:
<script setup>
import { ref } from 'vue'
const product = ref('T-Shirt')
const image = ref('/images/t-shirt-bleu.svg')
const inventory = ref(100)
const details = ref(['60 % coton', '30 % laine', '10 % polyester'])
</script>Then display it:
<ul>
<li v-for="detail in details" :key="detail">{{ detail }}</li>
</ul>Vue renders one <li> per entry. In the detail in details expression, details is the array and detail the alias for the current item, available anywhere inside the repeated element.
Looping over objects
The variants of the product are objects, with an id and a colour:
<script setup>
const variants = ref([
{ id: 2234, color: 'Bleu' },
{ id: 2235, color: 'Rouge' }
])
</script>Their properties are reached with the dot notation:
<div v-for="variant in variants" :key="variant.id">{{ variant.color }}</div>The index
A second parameter gives the position in the array, starting at zero:
<li v-for="(detail, index) in details" :key="detail">
{{ index + 1 }}. {{ detail }}
</li>Chapter 7 will use that index to work out which variant is being hovered.
The key attribute
You may have noticed the :key on every loop. It is the shorthand for v-bind:key seen in chapter 2, and it is not decoration.
When the list changes, Vue compares the old version with the new one to touch only what needs touching. Without a stable identifier, it matches items by position: insert an entry at the top of the list and it decides that every item has new content, and rewrites them all.
With a unique, stable key, it recognises each item and simply moves the ones that moved. That also avoids a classic bug: a text field or a ticked checkbox that stays attached to the wrong item after a sort.
Three rules for the key:
- Unique within the list.
- Stable: the same entry keeps the same key from one render to the next.
- Not the index, unless the list never changes. The index shifts on insert, delete and sort, which is exactly when you must not rely on it.
Use the identifier that comes with your data. Here, variant.id for the variants, and the string itself for the details since they are unique.
Since Vue 3, v-if is evaluated before v-for. The loop variable does not exist yet, so Vue throws an error along the lines of variant is not defined. Filter the list in a computed property, or move the loop onto an inner <template>.
<!-- ne fonctionne pas -->
<div v-for="variant in variants" v-if="variant.enStock">…</div>
<!-- fonctionne -->
<template v-for="variant in variants" :key="variant.id">
<div v-if="variant.enStock">…</div>
</template>Looping over something other than an array
v-for also accepts an object, a string, or a whole number:
<!-- objet : valeur, clé, index -->
<li v-for="(valeur, cle) in produit" :key="cle">{{ cle }} : {{ valeur }}</li>
<!-- nombre : de 1 à 5, pas de 0 à 4 -->
<span v-for="n in 5" :key="n">{{ n }}</span>The template is identical. Only the array declaration changes: data() { return { details: [...] } } instead of const details = ref([...]).
v-for. Then add a third colour variant to the array and check that it shows up with no other change.