Template Refs

Reach the real DOM element your template produced with useTemplateRef, and know when it holds an element and when it is null.

Syntaxconst 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

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

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

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

Example · vue
<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

  • Be the first to comment on this lesson.