Chapter 46: Vue ‘key’ Attribute
The :key attribute (also written as v-bind:key or just key=”…”)
If you forget :key (or use a bad one) in certain situations, Vue will give you very strange bugs — flickering UI, lost focus in inputs, wrong animations, duplicate items, state not preserved… and you will spend hours wondering “why is Vue behaving like this?!”
So let’s understand it properly — like a senior dev explaining to a junior over coffee.
What does :key actually do?
The :key attribute is Vue’s way of telling the virtual DOM diffing algorithm:
“This particular DOM element / component instance has a unique identity. When the list/order changes, please track this exact item by its key — don’t just look at position/index.”
In simple words:
- :key = permanent ID card for each item in a v-for loop
- Without it → Vue treats items as “the first one”, “the second one”, etc. (position-based)
- With it → Vue treats items as “this is item #XYZ”, even if it moves position
Why is this so important? Real bugs you will see without :key
Bug example 1 – Input focus lost
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
<ul> <!-- NO :key – bad! --> <li v-for="todo in todos"> <input type="text" v-model="todo.text" /> <button @click="todos.splice(todos.indexOf(todo), 1)">Delete</button> </li> </ul> |
→ You type in the second input → delete the first item → focus jumps or gets lost → Why? Vue reuses DOM elements based on position → the second <input> becomes the first one → browser loses focus
With correct :key
|
0 1 2 3 4 5 6 7 8 |
<li v-for="todo in todos" :key="todo.id"> <input type="text" v-model="todo.text" /> </li> |
→ Delete first item → focus stays exactly where it was (second input remains second)
Bug example 2 – Wrong animation / flickering
When using <TransitionGroup> without :key → items animate incorrectly or flicker because Vue can’t track “who is who”.
Bug example 3 – State lost in child components
|
0 1 2 3 4 5 6 |
<ItemComponent v-for="item in items" :item="item" /> |
→ If no :key, when list order changes → child components get reused in wrong order → internal state (checkbox checked, input value, scroll position) jumps to wrong item
Golden Rules for :key (Memorize These!)
| Rule # | Rule | Correct | Wrong / Dangerous | Why wrong is bad |
|---|---|---|---|---|
| 1 | Always use :key on v-for | :key=”todo.id” | no key at all | Vue uses index → bugs on reorder/delete |
| 2 | Key must be unique within the list | :key=”todo.id” (UUID or DB ID) | :key=”todo.text” (duplicates possible) | Duplicate keys → Vue gets confused |
| 3 | Key must be stable (same item = same key always) | :key=”todo.id” | :key=”index” | Index changes when items are added/removed → Vue thinks it’s a new item |
| 4 | Never use random values | :key=”Math.random()” | — | Every render → new key → item treated as completely new → no animation, state lost |
| 5 | Use primitive values (string/number) | :key=”todo.id” | :key=”todo” (object) | Objects compared by reference → fails if object recreated |
Real-World Patterns – What Experienced Devs Use in 2026
| Situation | Best :key choice | Example code snippet |
|---|---|---|
| Data from database/API | Database ID / UUID | :key=”todo.id” |
| Local-only items (no ID yet) | Date.now() + incremental counter | id: Date.now() |
| Static list (never changes order) | Index is acceptable (but still not ideal) | :key=”index” |
| List from external API | Use id field or generate UUID on client | :key="item.externalId |
| Polymorphic list (different types) | Combine type + ID | :key="\post-${item.id}“` |
Full Realistic Example – Animated Todo List with Proper :key
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<template> <div class="todo-app"> <h2>Todo List with Animations</h2> <div class="add-row"> <input v-model.trim="newTask" placeholder="New task..." @keyup.enter="addTask" /> <button @click="addTask">Add</button> </div> <!-- TransitionGroup + correct :key = smooth animations --> <TransitionGroup name="list" tag="ul"> <li v-for="todo in sortedTodos" :key="todo.id" <!-- ★ correct & stable key ★ --> class="todo-item" > <input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" /> <span :class="{ done: todo.done }">{{ todo.text }}</span> <button @click="remove(todo.id)">×</button> </li> </TransitionGroup> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue' interface Todo { id: number text: string done: boolean } const newTask = ref('') const todos = ref<Todo[]>([ { id: 1, text: 'Learn :key properly', done: true }, { id: 2, text: 'Never use index as key again', done: false } ]) const sortedTodos = computed(() => { return [...todos.value].sort((a, b) => (a.done === b.done ? 0 : a.done ? 1 : -1)) }) function addTask() { if (!newTask.value.trim()) return todos.value.push({ id: Date.now(), // simple stable ID for demo text: newTask.value.trim(), done: false }) newTask.value = '' } function toggle(id: number) { const todo = todos.value.find(t => t.id === id) if (todo) todo.done = !todo.done } function remove(id: number) { todos.value = todos.value.filter(t => t.id !== id) } </script> <style scoped> /* TransitionGroup classes */ .list-enter-active, .list-leave-active, .list-move { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .list-enter-from, .list-leave-to { opacity: 0; transform: translateX(-30px); } .list-leave-active { position: absolute; width: 100%; } </style> |
Quick Cheat Sheet – :key Do’s & Don’ts (Memorize This)
| Do this | Don’t do this | Why bad |
|---|---|---|
| :key=”item.id” | no :key at all | position-based → bugs on reorder/delete |
| :key=”item.uuid” | :key=”index” | index changes → Vue thinks it’s new item |
| :key=”item.id + item.type” | :key=”Math.random()” | random key every render → no animation, state lost |
| :key=”String(item.id)” | :key=”item” (whole object) | objects compared by reference → fails on new object instance |
Final 2026 Advice from Real Projects
- Always put :key on every v-for — even if list is static (future-proof)
- Use database ID / UUID whenever possible
- For local-only lists → Date.now() or incremental counter is fine
- Never use index as :key when list is dynamic (add/remove/reorder/sort/filter)
- Vue Devtools shows you warnings when :key is missing or duplicate — pay attention!
Your mini homework:
- Build a todo list without:key → add/delete/reorder items → see focus lost & animation bugs
- Add correct :key=”todo.id” → fix all bugs
- Try sorting the list → see how .list-move animation looks smooth
Any part still confusing? Want to see:
- :key + <TransitionGroup> + drag-and-drop (sortable list)?
- Common bug with :key=”index” + input focus?
- :key in nested v-for?
- Why :key matters even without animations?
Just tell me — we’ll debug and animate the next list together 🚀
Happy key-ing from Hyderabad! 💙
