Template Refs in Vue.js: Finding Your Elements Like a Treasure Map! 🗺️
Imagine you have a magic sticker you can put on anything in your room. Later, when you call out the sticker’s name, you instantly know exactly where that thing is! That’s what Template Refs do in Vue.js — they’re your magic stickers for HTML elements.
What Are Template Refs?
Think of your Vue template like a toy box full of different toys (elements). Sometimes you need to grab a specific toy directly — not through a helper, but with your own hands.
Template Refs let you put a “name tag” on any element so you can find it and play with it directly!
<input ref="myInput" />
That ref="myInput" is like putting a sticky note that says “myInput” on this input box.
Why Do We Need Them?
Most of the time, Vue handles everything for you. But sometimes you need to:
- Focus on an input box when a page loads
- Measure how big something is
- Control a video player (play, pause)
- Draw on a canvas
These need direct access — and refs give you that superpower!
Accessing Template Refs
The Two Ways: Options API vs Composition API
Options API (The Classic Way)
<template>
<input ref="nameInput" />
</template>
<script>
export default {
mounted() {
// Access it through this.$refs
this.$refs.nameInput.focus()
}
}
</script>
It’s like having a drawer called $refs where all your sticky-noted items go!
Composition API (The Modern Way)
<template>
<input ref="nameInput" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
// Create a "bucket" with the SAME name
const nameInput = ref(null)
onMounted(() => {
nameInput.value.focus()
})
</script>
The Golden Rule
The ref variable name in your script MUST match the ref attribute in your template!
graph TD A["Template: ref=&#39;box&#39;"] --> B{Names Match?} B -->|Yes: const box| C["Connected!"] B -->|No: const cube| D[Won't Work!]
When Can You Access Refs?
Important: Refs are only available AFTER the component mounts!
// ❌ WRONG - too early!
const myRef = ref(null)
console.log(myRef.value) // null
// âś… CORRECT - wait for mount
onMounted(() => {
console.log(myRef.value) // <div>...</div>
})
Think of it like this: You can’t find your toy until it’s actually IN the toy box!
Refs on Components
Here’s something cool — you can put refs on child components too, not just HTML elements!
<template>
<ChildComponent ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './Child.vue'
const childRef = ref(null)
onMounted(() => {
// Access the child component!
console.log(childRef.value)
})
</script>
Options API Components
If your child uses Options API, you get access to everything:
// Child.vue (Options API)
export default {
data() {
return { secretNumber: 42 }
},
methods: {
sayHello() { alert('Hello!') }
}
}
// Parent can access:
childRef.value.secretNumber // 42
childRef.value.sayHello() // Shows alert
Script Setup Components (Private by Default!)
But with <script setup>, things are private by default:
<!-- Child.vue -->
<script setup>
const secretNumber = ref(42)
const sayHello = () => alert('Hello!')
// Must EXPOSE what parents can see
defineExpose({
secretNumber,
sayHello
})
</script>
It’s like having a house with locked doors. You decide which rooms guests can enter!
graph TD A["Parent wants access"] --> B{Child uses script setup?} B -->|No| C["Full access to everything"] B -->|Yes| D{defineExpose used?} D -->|Yes| E["Access exposed items only"] D -->|No| F["No access - all private!"]
Refs Inside v-for
What if you have a list of items? You can’t use the same name for everything!
The Solution: Array of Refs
<template>
<ul>
<li
v-for="item in list"
:key="item.id"
ref="itemRefs"
>
{{ item.text }}
</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
{ id: 1, text: 'Apple' },
{ id: 2, text: 'Banana' },
{ id: 3, text: 'Cherry' }
])
// This becomes an ARRAY!
const itemRefs = ref([])
onMounted(() => {
console.log(itemRefs.value)
// [<li>, <li>, <li>] - All three!
})
</script>
Order Warning!
The array order might NOT match your list order!
Why? Because Vue fills the array as elements appear in the DOM, which might differ from your data order.
// If you need guaranteed order, use index:
<li
v-for="(item, index) in list"
:ref="el => itemRefs[index] = el"
>
Function Refs (More Control)
Instead of a name, use a function:
<template>
<div
v-for="item in list"
:key="item.id"
:ref="el => setItemRef(el, item.id)"
>
{{ item.text }}
</div>
</template>
<script setup>
const itemRefMap = ref({})
function setItemRef(el, id) {
if (el) {
itemRefMap.value[id] = el
}
}
// Now access by ID!
// itemRefMap.value[1] → first item
// itemRefMap.value[2] → second item
</script>
This is like giving each toy its own labeled box instead of throwing them all in a pile!
useTemplateRef (Vue 3.5+)
Vue 3.5 introduced a cleaner way with useTemplateRef():
<template>
<input ref="userInput" />
</template>
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// Name in useTemplateRef matches ref in template
const inputEl = useTemplateRef('userInput')
onMounted(() => {
inputEl.value.focus()
})
</script>
Why useTemplateRef?
| Old Way | useTemplateRef |
|---|---|
| Variable name must match ref | Can use any variable name! |
const userInput = ref(null) |
const inputEl = useTemplateRef('userInput') |
Benefit: Your variable name can be different from the template ref name. More flexibility!
<template>
<button ref="submitBtn">Submit</button>
<button ref="cancelBtn">Cancel</button>
</template>
<script setup>
import { useTemplateRef } from 'vue'
// Clear, descriptive variable names!
const submitButton = useTemplateRef('submitBtn')
const cancelButton = useTemplateRef('cancelBtn')
</script>
Quick Summary
| What | How | When to Use |
|---|---|---|
| Basic Ref | ref="name" + ref(null) |
Single element access |
| Component Ref | Same as above | Access child methods/data |
| defineExpose | In child component | Make script setup items public |
| v-for Refs | Returns array | Access multiple elements |
| Function Refs | :ref="el => ..." |
Custom ref handling |
| useTemplateRef | useTemplateRef('name') |
Vue 3.5+ cleaner syntax |
The Big Picture
graph TD A["Template Refs"] --> B["On Elements"] A --> C["On Components"] A --> D["In v-for Loops"] B --> E["Direct DOM Access"] C --> F["Child Instance Access"] D --> G["Array of Elements"] F --> H{script setup?} H -->|Yes| I["Use defineExpose"] H -->|No| J["Full Access"] G --> K["Function Refs for Control"]
Remember!
- Refs are for when Vue’s reactive system isn’t enough — direct DOM access
- Names must match (unless using useTemplateRef)
- Only available after mount — don’t access too early!
- script setup = private — use defineExpose to share
- v-for refs = array — order may vary
You now have the treasure map! Go find your elements! 🎉
