Lists and Keys
v-for — Rendering Lists
The v-for directive renders a list of items based on an array or object. Always provide a :key attribute with a unique, stable identifier — this helps Vue efficiently update the DOM when the list changes.
<template>
<div>
<!-- Array with index -->
<ul>
<li v-for="(fruit, index) in fruits" :key="fruit">
{{ index + 1 }}. {{ fruit }}
</li>
</ul>
<!-- Array of objects — use stable ID as key -->
<div v-for="user in users" :key="user.id" class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<!-- Object properties -->
<dl>
<template v-for="(value, key, index) in person" :key="key">
<dt>{{ index + 1 }}. {{ key }}</dt>
<dd>{{ value }}</dd>
</template>
</dl>
<!-- Range (1 to n) -->
<span v-for="n in 5" :key="n">{{ n }} </span>
<!-- Nested v-for -->
<div v-for="category in categories" :key="category.id">
<h3>{{ category.name }}</h3>
<ul>
<li v-for="item in category.items" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
<!-- Filtered list with computed -->
<input v-model="search" placeholder="Search..." />
<ul>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
<!-- v-for + v-if — use <template> to avoid conflict -->
<template v-for="user in users" :key="user.id">
<div v-if="user.isActive">{{ user.name }}</div>
</template>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
const fruits = ref(['Apple', 'Banana', 'Cherry', 'Date'])
const users = reactive([
{ id: 1, name: 'Alice', email: 'alice@example.com', isActive: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', isActive: false },
{ id: 3, name: 'Carol', email: 'carol@example.com', isActive: true },
])
const person = reactive({ name: 'Alice', age: 25, city: 'NYC' })
const categories = reactive([
{ id: 1, name: 'Fruits', items: [{ id: 11, name: 'Apple' }, { id: 12, name: 'Banana' }] },
{ id: 2, name: 'Veggies', items: [{ id: 21, name: 'Carrot' }] },
])
const search = ref('')
const filteredUsers = computed(() =>
users.filter(u => u.name.toLowerCase().includes(search.value.toLowerCase()))
)
</script>
<template>
<div>
<div class="controls">
<input v-model="newTask" @keyup.enter="addTask" placeholder="New task..." />
<select v-model="sortBy">
<option value="name">Sort by Name</option>
<option value="priority">Sort by Priority</option>
</select>
</div>
<TransitionGroup name="list" tag="ul">
<li v-for="task in sortedTasks" :key="task.id" class="task-item">
<input type="checkbox" v-model="task.done" />
<span :class="{ done: task.done }">{{ task.name }}</span>
<select v-model="task.priority">
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<button @click="removeTask(task.id)">✕</button>
</li>
</TransitionGroup>
<p>{{ tasks.filter(t => t.done).length }} / {{ tasks.length }} done</p>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
const newTask = ref('')
const sortBy = ref('name')
const tasks = reactive([
{ id: 1, name: 'Learn Vue', priority: 'high', done: false },
{ id: 2, name: 'Build app', priority: 'medium', done: false },
{ id: 3, name: 'Deploy', priority: 'low', done: false },
])
const sortedTasks = computed(() => {
return [...tasks].sort((a, b) => {
if (sortBy.value === 'name') return a.name.localeCompare(b.name)
const order = { high: 0, medium: 1, low: 2 }
return order[a.priority] - order[b.priority]
})
})
function addTask() {
if (!newTask.value.trim()) return
tasks.push({ id: Date.now(), name: newTask.value, priority: 'medium', done: false })
newTask.value = ''
}
function removeTask(id) {
const idx = tasks.findIndex(t => t.id === id)
if (idx !== -1) tasks.splice(idx, 1)
}
</script>
<style>
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(-20px); }
.list-move { transition: transform 0.3s ease; }
.done { text-decoration: line-through; opacity: 0.5; }
</style>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.