Template Refs
Reach the real DOM element your template produced with useTemplateRef, and know when it holds an element and when it is null.
const el = useTemplateRef<HTMLInputElement>('key') // Vue 3.5+A template ref gives you the actual DOM element a piece of template produced. You need it for the jobs a template cannot express: focusing an input, measuring a box, calling select() or scrollIntoView(). You mark the element with a ref attribute, then read the element from a ref object in script.
Two forms of the same thing
Since Vue 3.5, useTemplateRef('name') returns a readonly shallow ref holding the element whose ref attribute is "name". The older form, still everywhere in existing code, is a plain ref(null) whose variable name matches the attribute value: const input = ref(null) paired with ref="input". Vue populates both the same way at the same moment. The helper only frees the variable name from the template and gives you a ref you cannot overwrite by accident.
Timing
The ref is null until the component is mounted, because the element does not exist before that. Read it in onMounted, in an event handler, or after await nextTick() — never during setup. It becomes null again when the element is unmounted, including when a v-if around it turns false, so guard with ?. every time.
Inside v-for the ref holds an array of elements. That array is not guaranteed to be in the same order as the source list, so do not index into it to find a particular row. Use a function ref, :ref="(el) => ...", and store each element against its own id.
Example
<script setup lang="ts">
import { onMounted, ref, useTemplateRef } from 'vue'
const query = ref('')
const field = useTemplateRef<HTMLInputElement>('search-field')
onMounted(() => {
field.value?.focus()
})
function clear() {
query.value = ''
field.value?.focus()
}
</script>
<template>
<div class="search">
<input
ref="search-field"
v-model="query"
type="search"
placeholder="Search"
/>
<button type="button" @click="clear">Clear</button>
</div>
</template>
<style scoped>
.search {
display: flex;
gap: 0.5rem;
}
</style>When to use it
- Focus the first field of a form as soon as it appears on screen.
- Measure a container's height to decide whether to show a "show more" control.
- Scroll one specific row of a long list into view after the user picks it.
- Hand a canvas or div to a chart library that needs a DOM node.
More examples
The pre-3.5 form you will meet in older code
This is the same mechanism as useTemplateRef('field'), written the way every Vue 3 codebase before 3.5 had to write it: declare a ref initialised to null whose variable name is identical to the ref attribute's value, and Vue assigns the element into it on mount. Rename the variable and the ref silently stays null, which is the main reason the helper exists. Both forms work in 3.5; recognise this one when reading other people's components.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const field = ref<HTMLInputElement | null>(null)
onMounted(() => {
field.value?.focus()
})
</script>
<template>
<input ref="field" type="search" />
</template>
<style scoped>
input {
width: 12rem;
}
</style>One element out of a list, by id
A plain ref="items" under v-for collects the elements into an array, but the array order is not guaranteed to match the source list, so cells[2] is not reliably the third row. A function ref is called with each element as it mounts, and with null when it unmounts, so storing elements in a Map keyed by the row id keeps lookups correct through sorting, filtering and removal. Reach for this for scroll-into-view, per-row measurement, or focusing the row a user just added.
<script setup lang="ts">
import { ref, type ComponentPublicInstance } from 'vue'
interface Row {
id: number
label: string
}
const rows = ref<Row[]>([
{ id: 1, label: 'Alpha' },
{ id: 2, label: 'Beta' },
{ id: 3, label: 'Gamma' }
])
const cells = new Map<number, HTMLLIElement>()
function keep(id: number, el: Element | ComponentPublicInstance | null) {
if (el instanceof HTMLLIElement) {
cells.set(id, el)
} else {
cells.delete(id)
}
}
function reveal(id: number) {
cells.get(id)?.scrollIntoView({ block: 'nearest' })
}
</script>
<template>
<ul class="rows">
<li v-for="row in rows" :key="row.id" :ref="(el) => keep(row.id, el)">
{{ row.label }}
<button type="button" @click="reveal(row.id)">Reveal</button>
</li>
</ul>
</template>
<style scoped>
.rows {
max-height: 6rem;
overflow-y: auto;
}
</style>Passing the element to a browser API
IntersectionObserver, ResizeObserver, a video element's play(), a third-party editor: all of them want a real node, which is exactly what a template ref is for. The shape to copy is here — read the ref inside onMounted, bail out if it is null so TypeScript narrows it, and disconnect in onUnmounted. An observer that is never disconnected keeps the element and this component's state alive for as long as the page lives.
<script setup lang="ts">
import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
const visible = ref(false)
const panel = useTemplateRef<HTMLElement>('panel')
let observer: IntersectionObserver | undefined
onMounted(() => {
const el = panel.value
if (!el) return
observer = new IntersectionObserver((entries) => {
visible.value = entries.some((entry) => entry.isIntersecting)
})
observer.observe(el)
})
onUnmounted(() => {
observer?.disconnect()
})
</script>
<template>
<section ref="panel" class="panel">
{{ visible ? 'In view' : 'Scrolled away' }}
</section>
</template>
<style scoped>
.panel {
min-height: 40vh;
}
</style>
Discussion