Vue's declarative rendering covers most needs, but sometimes you must touch a real DOM element: focus an input, measure a box, scroll a list, or hand a canvas to a charting library. Template refs give you that access without breaking Vue's model. In this lesson you will obtain element and component references, understand when they become available, use nextTick to wait for DOM updates, and expose a child component's methods safely.
Add a ref="name" attribute to an element, then read it with useTemplateRef('name') (Vue 3.5+). The returned ref is null until the component is mounted:
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef('search')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="search" placeholder="Search" />
</template>Before Vue 3.5 the pattern was a plain ref whose variable name matches the attribute: const search = ref(null) with <input ref="search">. It still works, but useTemplateRef is explicit about intent and infers the element type in TypeScript.
Refs are only populated after mount, so accessing them directly in <script setup> top-level code gives null. Use onMounted, a watcher, or an event handler.
When ref is used on an element rendered by v-for, the ref holds an array of elements. The order is not guaranteed to match the source array, so use it for operations that do not depend on order, or attach data attributes:
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
const items = ref(['a', 'b', 'c'])
const rows = useTemplateRef('rows')
onMounted(() => console.log(rows.value.length)) // 3
</script>
<template>
<li v-for="item in items" :key="item" ref="rows">{{ item }}</li>
</template>A function ref gives full control: :ref="(el) => { if (el) registry.set(id, el) }". It is called with the element on mount and with null on unmount, which makes it suitable for keeping a map of elements keyed by id.
Vue batches state changes and updates the DOM asynchronously. If you change state and immediately read the DOM, you see the old version. nextTick returns a promise that resolves after the pending update has been applied:
<script setup>
import { ref, useTemplateRef, nextTick } from 'vue'
const messages = ref([])
const list = useTemplateRef('list')
async function add(text) {
messages.value.push(text)
await nextTick()
list.value.scrollTop = list.value.scrollHeight // scrolls to the new item
}
</script>
<template>
<ul ref="list" class="log">
<li v-for="(m, i) in messages" :key="i">{{ m }}</li>
</ul>
</template>The same applies when you show an element with v-if and want to focus it: set the flag, await nextTick(), then call focus().
A ref on a child component tag gives you the component instance. With <script setup>, components are closed by default: the parent sees nothing unless the child explicitly exposes it:
<!-- VideoPlayer.vue -->
<script setup>
import { useTemplateRef } from 'vue'
const video = useTemplateRef('video')
function play() {
video.value.play()
}
defineExpose({ play })
</script>
<template>
<video ref="video" src="/intro.mp4"></video>
</template>The parent calls player.value.play() after obtaining const player = useTemplateRef('player') and rendering <VideoPlayer ref="player" />. Expose only what is needed; imperative calls between components should be the exception, with props and events as the default communication channel.
autofocus, :class, v-show. Reach for refs only when the DOM API is the only way.null: a ref is null before mount, after unmount, and while its element is hidden by v-if.reactive state; a template ref is already the right container.new Chart(canvas.value, options).When does a template ref receive its element?
ref="name" to an element and read it with useTemplateRef('name'); the value is null until mount.v-for hold arrays; function refs give full control over registration.await nextTick() before reading the DOM after a state change.defineExpose lists.Next lesson: Composables: Reusable Logic with the Composition API — extract stateful logic into use* functions shared across components.