Template refs in Vue.js
Template refs in Vue.js give you direct access to DOM elements or child component instances from JavaScript.
Theory
TL;DR
- A template ref is a
ref()variable that Vue populates with the actual DOM node (or component instance) after the component mounts - The ref value is
nulluntil the component mounts - Inside
<script setup>, child components expose nothing by default - they needdefineExposeto share methods or data with a parent - Use refs for tasks Vue reactivity can't handle on its own: focus, scroll, canvas, initializing third-party libraries
- Reactive data solves most things. Refs are for the exceptions.
Quick example
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
// null before mount, HTMLInputElement after
inputRef.value?.focus()
})
</script>
<template>
<input ref="inputRef" type="text" />
</template>The string "inputRef" in the template matches the variable name in <script setup>. Vue connects them automatically.
When to use template refs
Refs make sense when Vue's data model can't solve the problem on its own:
- Focus an input on mount or after a user action
- Scroll to a specific element (
scrollIntoView) - Read element dimensions (
getBoundingClientRect,offsetHeight) - Draw on canvas (
getContext('2d')) - Initialize a third-party library (Chart.js, D3) that needs a real DOM node
If you can do it with v-model, :class, or computed data, do it that way instead.
Component refs and defineExpose
When you put ref on a child component, you get its public instance. But <script setup> components expose nothing by default. The child must declare what the parent can see:
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() { count.value = 0 }
defineExpose({ count, reset })
</script><!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
function handleReset() {
childRef.value?.reset()
}
</script>
<template>
<ChildComponent ref="childRef" />
<button @click="handleReset">Reset</button>
</template>Without defineExpose, childRef.value is an empty proxy. Calling any method on it does nothing.
Refs inside v-for
When ref sits on an element inside v-for, the ref variable becomes an array:
<script setup>
import { ref, onMounted } from 'vue'
const listRefs = ref([])
onMounted(() => {
listRefs.value[0]?.scrollIntoView()
})
</script>
<template>
<li v-for="item in items" :key="item.id" ref="listRefs">
{{ item.name }}
</li>
</template>The array order follows DOM order, not the order items were added.
Common mistakes
Accessing ref.value before mount. The ref is null outside onMounted or a later lifecycle hook. Most bugs here come from calling .focus() directly in the component body.
// Wrong - ref is null here
const inputRef = ref(null)
inputRef.value.focus() // TypeError: Cannot read properties of null
// Right
onMounted(() => {
inputRef.value?.focus()
})Forgetting that v-if resets the ref. When v-if evaluates to false, the element is removed from the DOM and the ref returns to null. When the condition becomes true again, you start a new mount cycle.
Missing defineExpose on the child. You attach a ref to a child component, try to call a method, and get nothing back. The child needs defineExpose({ methodName }).
Reading DOM state manually when reactivity would work. Grabbing inputRef.value.value to read input text and updating it directly - that is what v-model is for.
Follow-up questions
Q: Why is a template ref null before mount?
A: Vue builds the virtual DOM tree first, then commits it to the real DOM during the mount phase. Before that commit, there is no actual element to point to.
Q: What is the difference between ref="inputRef" and :ref="fn"?
A: The string version binds to the variable named inputRef in <script setup>. The bound version accepts a function that receives the element directly, which is useful for storing multiple refs in a Map.
Q: Can you use template refs with the Options API?
A: Yes. In Options API you access refs via this.$refs.inputRef. The behavior is the same - null before mount, populated after.
Q: What happens to a ref inside v-if when the condition toggles?
A: When the condition becomes false, Vue unmounts the element and sets the ref to null. When it becomes true again, Vue mounts a fresh element and repopulates the ref.
Examples
Focus management on form mount
A login form that focuses the email field immediately after mounting.
<script setup>
import { ref, onMounted } from 'vue'
const emailRef = ref(null)
onMounted(() => {
emailRef.value?.focus()
})
</script>
<template>
<form>
<input ref="emailRef" type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Log in</button>
</form>
</template>The ?. optional chaining guards against the edge case where the component unmounts before onMounted fires.
Initializing a chart on a canvas element
Third-party libraries like Chart.js need a real DOM node. Passing a reactive variable won't work here.
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Chart from 'chart.js/auto'
const canvasRef = ref(null)
let chart = null
onMounted(() => {
if (!canvasRef.value) return
chart = new Chart(canvasRef.value, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{ label: 'Sales', data: [12, 19, 8] }]
}
})
})
onUnmounted(() => {
chart?.destroy() // prevent memory leaks
})
</script>
<template>
<canvas ref="canvasRef" width="400" height="200" />
</template>I've seen teams try to make Chart.js work with reactive state alone. It never does. The canvas needs a real node, and a template ref is the only clean way to get there.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.