Chapter 34: Vue Template Refs
Template Refs (ref=”myElement” + const myElement = ref(null))
This is the official, recommended way to get direct access to a DOM element (or a component instance) from inside your Vue component.
In simple words:
Template refs let you say: “Hey Vue, please give me a reference (a handle) to this particular <div>, <input>, <canvas>, or even <MyCustomComponent> so I can talk to it directly in JavaScript when I need to.”
You use them when Vue’s declarative reactivity system is not enough and you need imperative control — things like:
- Focusing an input field
- Measuring element size / position
- Calling methods on a third-party library widget
- Integrating legacy JS libraries
- Animating or scrolling to a specific element
- Accessing canvas / video / audio APIs
Why Not Just Use document.getElementById()?
You could… but it’s very bad practice in Vue:
- Breaks reactivity & component encapsulation
- Fails in SSR / testing
- Doesn’t survive component re-renders
- Doesn’t work well with v-if / v-for (element may not exist yet)
- Hard to clean up
Template refs are reactive, scoped to the component, safe, and automatically cleaned up.
Modern Vue 3 Syntax (2026 Standard – <script setup>)
There are two common patterns — both are correct, but one is cleaner.
Pattern 1 – Single element (most common)
|
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 |
<template> <div> <input ref="usernameInput" type="text" placeholder="Enter username" /> <button @click="focusInput"> Focus Input </button> </div> </template> <script setup> import { ref, onMounted } from 'vue' // 1. Create a ref (starts as null) const usernameInput = ref(null) // or ref<HTMLInputElement | null>(null) with TS function focusInput() { // 2. Access the real DOM element if (usernameInput.value) { usernameInput.value.focus() usernameInput.value.select() // bonus: highlight text } } // 3. Optional: do something when component mounts onMounted(() => { // usernameInput.value is now the <input> element console.log('Input element:', usernameInput.value) }) </script> |
Pattern 2 – Multiple elements (v-for or dynamic list)
|
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 |
<template> <div> <h2>Todo List</h2> <ul> <li v-for="(todo, index) in todos" :key="todo.id" :ref="el => { todoRefs[index] = el }" > {{ todo.text }} </li> </ul> <button @click="scrollToLast"> Scroll to Last Todo </button> </div> </template> <script setup> import { ref, onMounted, nextTick } from 'vue' const todos = ref([ { id: 1, text: 'Learn template refs' }, { id: 2, text: 'Build something real' }, { id: 3, text: 'Master Vue 3' } ]) // Array of refs (one per <li>) const todoRefs = ref([]) // Function to scroll to the last item async function scrollToLast() { await nextTick() // wait for DOM to update if needed const lastTodo = todoRefs.value[todoRefs.value.length - 1] if (lastTodo) { lastTodo.scrollIntoView({ behavior: 'smooth', block: 'center' }) } } </script> |
Important Rules & Gotchas (2026 Edition)
| Situation / Question | Behavior / Solution |
|---|---|
| When is ref.value populated? | After component is mounted (onMounted) — before that it’s null |
| What if element is inside v-if? | ref.value becomes null when condition is false — check if (ref.value) |
| Multiple elements with same ref name? | ref.value becomes an array of elements |
| v-for + ref | Use callback style :ref=”el => todoRefs[index] = el” or ref array + el => … |
| Accessing child component instance? | ref.value is the component instance — you can call exposed methods |
| Cleanup? | Vue automatically cleans up — ref.value becomes null on unmount |
| TypeScript? | `const el = ref<HTMLElement |
| SSR / hydration? | Refs are null on server — only populated client-side |
Real-World Power Example – Accessing Child Component Methods
Child: VideoPlayer.vue
|
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 |
<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() } // Expose methods to parent via ref defineExpose({ play, pause }) </script> <template> <video ref="video" controls src="https://example.com/demo.mp4"></video> </template> |
Parent
|
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="playerRef" /> <button @click="playerRef?.play()">Play from parent</button> <button @click="playerRef?.pause()">Pause from parent</button> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import VideoPlayer from '@/components/VideoPlayer.vue' const playerRef = ref<InstanceType<typeof VideoPlayer> | null>(null) </script> |
→ Parent can call play() / pause() on the child component instance
Summary Table – Template Refs Cheat Sheet
| Goal | Code Pattern | When to use it |
|---|---|---|
| Focus input / textarea | ref=”inputEl” → inputEl.value.focus() | Form UX, autofocus |
| Measure element size / position | onMounted(() => { const rect = el.value.getBoundingClientRect() }) | Custom tooltips, popovers |
| Scroll to element | el.value.scrollIntoView() | Jump links, last message in chat |
| Access canvas / video / audio API | ref=”canvas” → canvas.value.getContext(‘2d’) | Charts, games, media players |
| Call method on child component | defineExpose({ myMethod }) + childRef.value.myMethod() | Imperative control |
| Multiple refs in v-for | :ref=”el => refsArray[index] = el” | Lists, carousels, accordions |
Pro Tips from Real Projects (2026)
- Prefer declarative solutions (v-model, v-if, transitions) → use refs only when you must go imperative
- Always check if (ref.value) — refs are null before mount and after unmount
- Use nextTick() when manipulating DOM after state change
- For third-party libraries (Leaflet, Chart.js, Swiper…) → refs are almost always the integration point
- Don’t overuse — too many refs = code smells like jQuery days
Your mini homework:
- Create ChatWindow.vue with many messages in v-for
- Add ref to last message
- When new message arrives → scroll to bottom using scrollIntoView()
- Add button “Scroll to top” using another ref
Any part confusing? Want to see:
- Refs + <Transition> for animated lists?
- Accessing child component with TypeScript?
- Common gotcha with v-if + ref?
- Real Chart.js integration with ref?
Just tell me — we’ll build the next practical example together 🚀
Happy reffing from Hyderabad! 💙
