Skip to main content

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 null until the component mounts
  • Inside <script setup>, child components expose nothing by default - they need defineExpose to 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

vue
<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:

vue
<!-- ChildComponent.vue --> <script setup> import { ref } from 'vue' const count = ref(0) function reset() { count.value = 0 } defineExpose({ count, reset }) </script>
vue
<!-- 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:

vue
<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.

js
// 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.

vue
<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.

vue
<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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?