Chapter 47: Vue ‘ref’ Attribute
The ref attribute (written as ref=”someName” in the template)
This is not the same as the ref() function you use for reactive state (although both are called “ref” — yes, Vue naming is sometimes confusing on purpose).
Template ref = a way to get a direct reference (a handle) to a real DOM element (or to a Vue component instance) so you can talk to it imperatively in JavaScript.
In plain English:
“Hey Vue, please give me a way to find and control this particular <input>, <div>, <canvas>, or even <MyVideoPlayer> component later when I need to do something to it directly.”
You use template refs when Vue’s normal declarative reactivity (v-model, v-if, v-for, etc.) is not enough and you need imperative DOM access or component instance access.
Common Real-World Situations Where You Need Template Refs
- Focus an input field automatically or on button click
- Scroll to a specific message in a chat
- Measure the size/position of an element (for custom tooltips, popovers)
- Call methods on a third-party library widget (Chart.js, Leaflet map, Swiper carousel…)
- Play/pause a <video> or <audio> element
- Get canvas context (getContext(‘2d’))
- Call exposed methods on a child component
- Integrate legacy non-Vue JavaScript libraries
Modern Vue 3 Syntax (2026 – <script setup>)
There are two main patterns — both are correct, but one is cleaner and more common.
Pattern 1 – Single element / component (90% of cases)
|
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 |
<template> <div class="demo"> <input ref="searchInput" type="text" placeholder="Type to search..." /> <button @click="focusAndSelect"> Focus & Select Text </button> <button @click="clearInput"> Clear </button> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' // 1. Declare a ref — it starts as null const searchInput = ref<HTMLInputElement | null>(null) // 2. Use it imperatively function focusAndSelect() { if (searchInput.value) { searchInput.value.focus() searchInput.value.select() // highlight all text } } function clearInput() { if (searchInput.value) { searchInput.value.value = '' searchInput.value.focus() } } // 3. Safe to use after mount onMounted(() => { console.log('Input element is now available:', searchInput.value) // Example: auto-focus on mount searchInput.value?.focus() }) </script> <style scoped> .demo { padding: 2rem; text-align: center; } input { padding: 0.8rem; width: 300px; font-size: 1.1rem; } button { margin: 0.5rem; padding: 0.8rem 1.5rem; } </style> |
Pattern 2 – Multiple refs in a v-for loop
|
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 |
<template> <div class="chat-window"> <div v-for="(message, index) in messages" :key="message.id" ref="messageRefs" class="message" :class="{ 'from-me': message.fromMe }" > {{ message.text }} </div> <button @click="scrollToBottom"> Scroll to Latest Message </button> </div> </template> <script setup lang="ts"> import { ref, onMounted, nextTick, watch } from 'vue' const messages = ref([ { id: 1, text: 'Hello!', fromMe: false }, { id: 2, text: 'Hi there!', fromMe: true }, // ... more messages ]) // Array of refs — one per message div const messageRefs = ref<HTMLElement[]>([]) // Scroll to the very last message async function scrollToBottom() { await nextTick() // wait for DOM to update const lastMessage = messageRefs.value[messageRefs.value.length - 1] if (lastMessage) { lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' }) } } // Auto-scroll when new message arrives watch(messages, async () => { await nextTick() scrollToBottom() }, { deep: true }) </script> |
Very Important Rules & Gotchas (2026 Edition)
| Rule / Gotcha | Correct Behavior / Solution |
|---|---|
| When is ref.value filled? | After the component is mounted — before that it’s null |
| What if element is inside v-if? | ref.value becomes null when condition is false — always check if (ref.value) |
| Multiple elements with same ref name? | ref.value becomes an array automatically |
| v-for + ref | Use callback syntax :ref=”(el) => messageRefs[index] = el” or just ref=”messageRefs” |
| Accessing child component instance | ref.value is the Vue component instance — you can call exposed methods |
| Cleanup? | Vue automatically sets ref.value = null on unmount — no manual cleanup needed |
| TypeScript typing | `ref<HTMLInputElement |
| SSR / hydration? | Refs are null on server — only populated client-side |
Real-World Power Example – Accessing Child Component Instance
Child: VideoPlayer.vue (exposes controls)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<script setup lang="ts"> import { ref, defineExpose } from 'vue' const video = ref<HTMLVideoElement | null>(null) function play() { video.value?.play() } function pause() { video.value?.pause() } defineExpose({ play, pause }) // ← make methods available via ref </script> <template> <video ref="video" controls src="https://example.com/demo.mp4"></video> </template> |
Parent – controls child imperatively
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<template> <div> <VideoPlayer ref="player" /> <button @click="player?.play()">Play from parent</button> <button @click="player?.pause()">Pause from parent</button> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import VideoPlayer from '@/components/VideoPlayer.vue' const player = ref<InstanceType<typeof VideoPlayer> | null>(null) </script> |
Quick Cheat Sheet – When to Use Template Refs
| Goal | Typical Code Pattern | When ref.value is safe? |
|---|---|---|
| Auto-focus input on mount | ref=”input” + onMounted(() => input.value.focus()) | After onMounted |
| Scroll to specific message | ref=”lastMsg” + lastMsg.value.scrollIntoView() | After mount / nextTick |
| Get element dimensions | const rect = el.value.getBoundingClientRect() | After mount |
| Control 3rd-party widget | ref=”chart” + chart.value?.chartInstance.update() | After mount |
| Call exposed method on child component | defineExpose({ play }) + child.value.play() | After mount |
| Multiple refs in loop | ref=”items” → items.value[index] | After mount |
Pro Tips from Real Projects (Hyderabad 2026)
- Always check if (ref.value) — refs are null before mount and after unmount
- Use nextTick() when you change state and immediately need updated DOM
- Prefer declarative solutions (v-model, v-if, transitions) → use refs only when you must go imperative
- For third-party libraries (Chart.js, Mapbox, Swiper, Video.js…) → template refs are usually the main integration point
- In TypeScript → type your refs → avoids “Property ‘focus’ does not exist on type ‘Ref<null>'” errors
Your mini practice task:
- Create an input that auto-focuses on mount (template ref + onMounted)
- Add a long scrollable list with many items
- Add a “Jump to bottom” button using a ref on the last item
- Add a button that focuses the input again
Any part confusing? Want full examples for:
- Template refs + <Transition> for animated focus?
- Accessing child component methods with TypeScript?
- Common bug with v-if + ref?
- Real Chart.js / Google Maps integration using refs?
Just tell me — we’ll build the next practical ref example together 🚀
Happy reffing from Hyderabad! 💙
