| |

Vue.js 40 🟢 Obtaining Template References using useTemplateRef()

The declarative model in Vue abstracts away the direct DOM manipulation for most cases, but there are times when you need the actual element. You need to focus an input when the component mounts. You need to measure the height of a div to set the scroll position. You need to pass a DOM node to a third-party library like a charting tool or a map. The ref attribute is the bridge. It marks an element, and Vue gives you the reference after the component mounts. In Vue 3.5, the useTemplateRef() composable replaced the older pattern of matching a ref() variable name to the ref attribute string, and it did so with better TypeScript support and fewer silent failures .

The older pattern was fragile. You declared a ref with a name that had to match the string in the template exactly. If you renamed the variable during a refactor and forgot to update the template, the ref stayed null with no error and no warning . The useTemplateRef() composable changes this. You pass the string name as an argument, and you get back a typed ref. The variable name and the template string are connected by the argument you pass, not by a naming coincidence. If you typo the string in the argument, the IDE can often catch it, and the connection is explicit .

This chapter covers three areas. First, why useTemplateRef() exists — the fragility of the old pattern and the type-safety problem. Second, how to use it — the syntax, the timing of the ref, and the difference between DOM element refs and component refs. Third, the advanced patterns — the v-for limitation, the function refs, and the cleanup. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the ref lifecycle.

Key point: useTemplateRef() is the Vue 3.5+ composable for obtaining template refs. You pass it the string name that matches the ref attribute in the template, and it returns a typed ref that is populated after the component mounts. It replaces the fragile pattern of declaring a ref() with a matching name. It does not work with v-for arrays; the legacy ref() pattern is still required for that case.


Why useTemplateRef exists

The fragile-naming problem. Before Vue 3.5, the connection between a ref variable and a ref attribute was a naming convention with no enforcement. You wrote const input = ref(null) in the script, and you wrote <input ref="input" /> in the template. If the names matched, the ref was populated. If they did not match — because of a typo, a refactor, or a copy-paste error — the ref stayed null. There was no compile error, no runtime warning, and no hint that anything was wrong. The element simply never appeared, and debugging required reading both files carefully .

The type-inference problem. With the old pattern, the TypeScript type of the ref had to be declared manually. You wrote const input = ref<HTMLInputElement | null>(null), and you were responsible for getting the type right. If you declared it as HTMLInputElement but the template had a <div>, the compiler did not know. The type was a manual annotation, not an inference from the template. Vue 3.5’s IDE support and vue-tsc can infer the type of the ref from the matching element in the template, but only when useTemplateRef() is used, because the argument connects the ref to the template string in a way the tooling can analyze .

The explicit-binding problem. useTemplateRef('my-input') makes the binding explicit. The string in the argument is the connection to the template. The variable name is free. You can name it inputElement, searchBox, or anything else. The ref is populated based on the argument, not the variable name. This decouples the script variable from the template string, which means refactoring is safer. Renaming the variable does not break the connection. Renaming the template string requires changing the argument, and the mismatch is a visible change in the code, not a silent failure .

The linter-enforcement problem. The Vue ESLint plugin added the prefer-use-template-ref rule, which enforces the new API over the old ref/shallowRef pattern for template refs. This rule was introduced in eslint-plugin-vue v9.31.0. The rule skips function refs, which are the case where the ref attribute is bound to a function rather than a string. For string-based refs, the linter pushes the codebase toward the more explicit and type-safe API .

The trade-off. useTemplateRef() adds a small amount of code compared to the old pattern. It requires Vue 3.5 or later. It does not work with v-for refs, which still require the legacy ref([]) pattern. The trade-off is between the brevity of the old pattern and the explicitness and type-safety of the new one. For the projects on Vue 3.5+, the new API is the recommended choice.


a. The basic syntax

The useTemplateRef() composable is imported from vue. It takes a string argument that must match the ref attribute value in the template. It returns a readonly shallow ref that is populated after the component mounts .

<script setup>
import { useTemplateRef, onMounted } from 'vue';

const input = useTemplateRef('my-input');

onMounted(() => {
  input.value.focus();
});
</script>

<template>
  <input ref="my-input" />
</template>

The 'my-input' string is the connection. The <input> has ref="my-input". After the component mounts, input.value holds the HTMLInputElement. The onMounted hook is the correct place to access it, because the element does not exist before the mount .

The type inference. When using TypeScript, the IDE and vue-tsc automatically infer the type of input.value based on the element the ref attribute is used on. An <input> gives HTMLInputElement. A <div> gives HTMLDivElement. A child component gives the component instance type .

The reactivity. The ref returned by useTemplateRef() is a readonly shallow ref. It is not deeply reactive. Watching it deeply does not work by design. The ref’s value changes when the element mounts or unmounts, but changes to the element’s properties do not trigger reactivity .

The timing. The ref is null on the first render. It is populated after the component mounts. Accessing it in a template expression before the mount returns null. Accessing it in onMounted or later lifecycle hooks returns the element .


b. Component refs and defineExpose

The ref attribute can also be used on a child component. In that case, the ref holds the component instance, not a DOM element .

<script setup>
import { useTemplateRef, onMounted } from 'vue';
import Child from './Child.vue';

const childRef = useTemplateRef('child');

onMounted(() => {
  // childRef.value holds the <Child /> instance
});
</script>

<template>
  <Child ref="child" />
</template>

The private-by-default problem. A child component using <script setup> is private by default. The parent cannot access anything on the child instance unless the child exposes it with defineExpose .

<!-- Child.vue -->
<script setup>
import { ref } from 'vue';

const count = ref(0);

function increment() {
  count.value++;
}

defineExpose({
  count,
  increment,
});
</script>

The parent’s childRef.value now has the shape { count: number, increment: () => void }. The refs inside defineExpose are automatically unwrapped, just like on normal instances .

The timing of defineExpose. defineExpose must be called before any await operation. Properties and methods exposed after an await will not be accessible to the parent .

The tightly-coupled warning. The Vue documentation warns that component refs create tightly-coupled implementation details between the parent and the child. The recommendation is to implement parent-child interactions using the standard props and emit interfaces first, and to use component refs only when absolutely needed .


c. The v-for limitation and function refs

useTemplateRef() does not work with v-for refs. When ref is used inside a v-for, Vue populates the ref with an array of elements. The useTemplateRef() API was designed for single element references and does not support the array population mechanism. You must use the legacy ref([]) pattern for v-for refs .

<script setup>
import { ref, onMounted } from 'vue';

const list = ref([1, 2, 3]);
const itemRefs = ref([]);

onMounted(() => {
  console.log(itemRefs.value); // an array of elements
});
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs" :key="item">
      {{ item }}
    </li>
  </ul>
</template>

The order problem. The ref array from v-for does not guarantee the same order as the source array. You should not assume that itemRefs.value[0] corresponds to list.value[0]. To correlate refs with data, use data attributes or function refs .

The function-ref pattern. The ref attribute can be bound to a function instead of a string. The function receives the element reference as its first argument, and it is called on every update. This gives full control over where the reference is stored .

<script setup>
import { ref, onBeforeUpdate } from 'vue';

const itemRefs = ref(new Map());

function setItemRef(el, item) {
  if (el) {
    itemRefs.value.set(item, el);
  } else {
    itemRefs.value.delete(item);
  }
}

onBeforeUpdate(() => {
  itemRefs.value.clear();
});
</script>

<template>
  <ul>
    <li
      v-for="item in items"
      :key="item"
      :ref="(el) => setItemRef(el, item)"
    >
      {{ item }}
    </li>
  </ul>
</template>

The onBeforeUpdate clears the map before each update, so removed elements are not retained. The setItemRef function stores the element by its associated data item, not by index. This is the pattern for the cases where the order matters or where the elements need to be correlated with the data .

The cleanup. When using refs with third-party libraries, clean up in onUnmounted or the component’s cleanup phase. The ref is null when the element is unmounted .


Complete Example Session

<!-- ============================================
     PART 1: THE BASIC DOM ELEMENT REF
     ============================================ -->

<script setup>
import { useTemplateRef, onMounted } from 'vue';

const input = useTemplateRef('my-input');

onMounted(() => {
  input.value.focus();
});
</script>

<template>
  <input ref="my-input" />
</template>


<!-- ============================================
     PART 2: THE MEASUREMENT USE CASE
     ============================================ -->

<script setup>
import { useTemplateRef, onMounted } from 'vue';

const infoBox = useTemplateRef('info-box');

onMounted(() => {
  console.log(infoBox.value?.clientHeight);
});
</script>

<template>
  <div ref="info-box">Content</div>
</template>


<!-- ============================================
     PART 3: THE TYPE INFERENCE
     ============================================ -->

<script setup lang="ts">
import { useTemplateRef } from 'vue';

// input.value is inferred as HTMLInputElement | null
const input = useTemplateRef('my-input');

// div.value is inferred as HTMLDivElement | null
const div = useTemplateRef('my-div');
</script>

<template>
  <input ref="my-input" />
  <div ref="my-div"></div>
</template>


<!-- ============================================
     PART 4: THE COMPONENT REF
     ============================================ -->

<!-- Child.vue -->
<script setup>
import { ref } from 'vue';

const count = ref(0);

function increment() {
  count.value++;
}

defineExpose({
  count,
  increment,
});
</script>

<!-- Parent.vue -->
<script setup>
import { useTemplateRef, onMounted } from 'vue';
import Child from './Child.vue';

const childRef = useTemplateRef('child');

onMounted(() => {
  childRef.value.increment();
  console.log(childRef.value.count); // 1
});
</script>

<template>
  <Child ref="child" />
</template>


<!-- ============================================
     PART 5: THE v-for LIMITATION
     ============================================ -->

<!-- useTemplateRef does NOT work with v-for -->
<script setup>
import { ref, onMounted } from 'vue';

const list = ref([1, 2, 3]);
const itemRefs = ref([]);

onMounted(() => {
  console.log(itemRefs.value); // an array of elements
});
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs" :key="item">
      {{ item }}
    </li>
  </ul>
</template>


<!-- ============================================
     PART 6: THE FUNCTION REF
     ============================================ -->

<script setup>
import { ref, onBeforeUpdate } from 'vue';

const itemRefs = ref(new Map());

function setItemRef(el, item) {
  if (el) {
    itemRefs.value.set(item, el);
  } else {
    itemRefs.value.delete(item);
  }
}

onBeforeUpdate(() => {
  itemRefs.value.clear();
});
</script>

<template>
  <ul>
    <li
      v-for="item in items"
      :key="item"
      :ref="(el) => setItemRef(el, item)"
    >
      {{ item }}
    </li>
  </ul>
</template>


<!-- ============================================
     PART 7: THE WATCH ON A TEMPLATE REF
     ============================================ -->

<script setup>
import { useTemplateRef, watchEffect } from 'vue';

const input = useTemplateRef('my-input');

watchEffect(() => {
  if (input.value) {
    input.value.focus();
  } else {
    // not mounted yet, or the element was unmounted
  }
});
</script>


<!-- ============================================
     PART 8: THE THIRD-PARTY LIBRARY INIT
     ============================================ -->

<script setup>
import { useTemplateRef, onMounted, onUnmounted } from 'vue';

const mapContainer = useTemplateRef('map-container');
let mapInstance = null;

onMounted(() => {
  mapInstance = new LeafletMap(mapContainer.value);
});

onUnmounted(() => {
  mapInstance?.remove();
  mapInstance = null;
});
</script>

<template>
  <div ref="map-container" style="height: 400px;"></div>
</template>


<!-- ============================================
     PART 9: THE MIGRATION FROM THE OLD PATTERN
     ============================================ -->

<!-- BEFORE (Vue < 3.5) -->
<script setup>
import { ref, onMounted } from 'vue';

const input = ref(null); // the name must match the template

onMounted(() => {
  input.value.focus();
});
</script>

<template>
  <input ref="input" />
</template>

<!-- AFTER (Vue 3.5+) -->
<script setup>
import { useTemplateRef, onMounted } from 'vue';

const input = useTemplateRef('my-input'); // any variable name

onMounted(() => {
  input.value.focus();
});
</script>

<template>
  <input ref="my-input" />
</template>


<!-- ============================================
     PART 10: THE COMPLETE EXAMPLE
     ============================================ -->

<script setup lang="ts">
import { useTemplateRef, onMounted, onUnmounted } from 'vue';

const searchInput = useTemplateRef('search-box');
const resultsContainer = useTemplateRef('results-container');

onMounted(() => {
  searchInput.value?.focus();

  const height = resultsContainer.value?.clientHeight ?? 0;
  console.log(`The results container is ${height}px tall`);
});

onUnmounted(() => {
  // the cleanup
});
</script>

<template>
  <div>
    <input ref="search-box" type="search" placeholder="Search..." />
    <div ref="results-container">
      <p>Results</p>
    </div>
  </div>
</template>

The ten parts show the basic DOM element ref, the measurement use case, the type inference, the component ref with defineExpose, the v-for limitation, the function ref pattern, the watch on a template ref, the third-party library initialization, the migration from the old pattern, and the complete example.


Quick Reference

The useTemplateRef API

AspectDetail
Importimport { useTemplateRef } from 'vue'
ArgumentThe string matching the ref attribute
ReturnsA readonly shallow ref
Initial valuenull
PopulatedAfter the component mounts
Type inferenceAutomatic from the template element
Vue version3.5+

The ref Attribute

UsageThe ref holds
<input ref="name" />The HTMLInputElement
<div ref="name" />The HTMLDivElement
<Child ref="name" />The Child component instance
<li v-for="..." ref="name">An array of elements

The Timing

The phaseThe ref value
The first rendernull
The mountThe element
The unmountnull

The Component Refs

The requirementThe detail
<script setup>Private by default
defineExposeExposes the public interface
The timingBefore any await
The accessThe parent reads the exposed shape

The v-for Refs

AspectThe detail
useTemplateRef()Does not work
The legacy patternconst refs = ref([])
The orderNot guaranteed
The fixThe function ref or the data attributes

Best Practices

✅ Do This:

<script setup>
// Use useTemplateRef with the explicit string
const input = useTemplateRef('my-input');                                 // ✅
// Access the ref in onMounted or later
onMounted(() => { input.value?.focus(); });                               // ✅
// Use defineExpose for the component refs
defineExpose({ count, increment });                                       // ✅
// Use the legacy ref for v-for
const itemRefs = ref([]);                                                 // ✅
// Use the function ref for the ordered access
:ref="(el) => setItemRef(el, item)"                                       // ✅
// Clean up the third-party libraries
onUnmounted(() => { mapInstance?.remove(); });                            // ✅
</script>

❌ Don’t Do This:

<script setup>
// Don't use useTemplateRef with v-for
const itemRefs = useTemplateRef('items'); // returns null                    // ❌
// Don't assume the v-for ref order matches the source
itemRefs.value[0]; // may not be the first item                            // ❌
// Don't access the ref before the mount
const input = useTemplateRef('my-input');
input.value.focus(); // null                                               // ❌
// Don't forget the defineExpose
// The parent cannot access the private child                               // ❌
// Don't use the old pattern in the Vue 3.5+ projects
const input = ref(null); // the name must match                            // ❌
// Don't watch a useTemplateRef deeply
watch(itemRefs, () => {}, { deep: true }); // the shallow ref              // ❌
</script>

Common Pitfalls

The pitfallThe whyThe fix
The ref is nullThe access before the mountUse onMounted
The v-for ref is nulluseTemplateRef with v-forUse ref([])
The ref order is wrongThe v-for orderUse the function ref or the data attributes
The child is privateNo defineExposeAdd the defineExpose
The type is unknownThe old patternUse useTemplateRef
The ref is not reactiveThe shallow refWatch the source, not the ref

Real-World Examples

1. The Focus on Mount

const input = useTemplateRef('input');
onMounted(() => input.value?.focus());

2. The Measurement

const box = useTemplateRef('box');
onMounted(() => console.log(box.value?.clientHeight));

3. The Component Ref

const child = useTemplateRef('child');
onMounted(() => child.value.increment());

4. The v-for Legacy

const itemRefs = ref([]);

5. The Function Ref

:ref="(el) => setItemRef(el, item)"

6. The Third-Party Init

onMounted(() => { mapInstance = new LeafletMap(mapContainer.value); });

7. The Cleanup

onUnmounted(() => { mapInstance?.remove(); });

8. The Type Inference

const input = useTemplateRef('my-input'); // HTMLInputElement

9. The Watch with the Null Check

watchEffect(() => { if (input.value) input.value.focus(); });

10. The Complete

const searchInput = useTemplateRef('search-box');
onMounted(() => searchInput.value?.focus());

Visual

The Ref Lifecycle

┌──────────────────────────────────────────────────────────────┐
│  THE REF LIFECYCLE                                           │
│                                                              │
│  The setup:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const input = useTemplateRef('my-input');           │    │
│  │    → the input.value is null                         │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  the render                                           │
│    ▼                                                         │
│  The template:                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <input ref="my-input" />                            │    │
│  │    → the element is created                          │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  the mount                                            │
│    ▼                                                         │
│  The onMounted:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  input.value.focus();                                │    │
│  │    → the input.value is the element                  │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  the unmount                                          │
│    ▼                                                         │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  input.value is null again                           │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Old vs the New Pattern

┌──────────────────────────────────────────────────────────────┐
│  THE OLD vs THE NEW PATTERN                                  │
│                                                              │
│  THE OLD (Vue < 3.5):                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const input = ref(null);                            │    │
│  │    → the variable name MUST match                    │    │
│  │                                                       │    │
│  │  <input ref="input" />                               │    │
│  │                                                       │    │
│  │  The typo → the silent null                          │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE NEW (Vue 3.5+):                                         │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const input = useTemplateRef('my-input');           │    │
│  │    → the argument is the connection                  │    │
│  │    → the variable name is free                       │    │
│  │                                                       │    │
│  │  <input ref="my-input" />                            │    │
│  │                                                       │    │
│  │  The typo → the visible mismatch                     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Component Ref

┌──────────────────────────────────────────────────────────────┐
│  THE COMPONENT REF                                           │
│                                                              │
│  The child:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <script setup>                                      │    │
│  │  const count = ref(0);                               │    │
│  │  function increment() { count.value++; }             │    │
│  │                                                       │    │
│  │  defineExpose({ count, increment });                 │    │
│  │  </script>                                           │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  the exposed interface                                │
│    ▼                                                         │
│  The parent:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const child = useTemplateRef('child');              │    │
│  │                                                       │    │
│  │  <Child ref="child" />                               │    │
│  │                                                       │    │
│  │  child.value.increment();                            │    │
│  │    → the exposed method                              │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The private-by-default child exposes only the defineExpose. │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The v-for Limitation

┌──────────────────────────────────────────────────────────────┐
│  THE v-for LIMITATION                                        │
│                                                              │
│  ❌ useTemplateRef does NOT work:                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const itemRefs = useTemplateRef('items');           │    │
│  │  → the itemRefs.value is null                        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ✅ The legacy pattern:                                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const itemRefs = ref([]);                           │    │
│  │  → the itemRefs.value is an array                    │    │
│  │                                                       │    │
│  │  <li v-for="item in list" ref="itemRefs">            │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The array order is NOT guaranteed.                  │    │
│  │  Use the function ref or the data attributes for     │    │
│  │  the correlation.                                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
useTemplateRef()The Vue 3.5+ composable for template refs
The argumentThe string matching the ref attribute
The returnA readonly shallow ref
The initial valuenull
The populatedAfter the component mounts
The type inferenceAutomatic from the template element
The component refsPrivate by default; require defineExpose
The v-for refsDo not work with useTemplateRef
The legacy v-forconst refs = ref([])
The orderNot guaranteed in v-for
The function ref:ref="(el) => setItemRef(el, item)"

Key takeaways:

  • useTemplateRef() is the Vue 3.5+ composable for obtaining template refs. You pass it the string that matches the ref attribute in the template, and it returns a ref populated after the component mounts.
  • It replaces the fragile naming-convention pattern. The old pattern required a ref() variable name to match the ref attribute exactly. A typo or refactor caused a silent null. The new API makes the connection explicit through the argument.
  • The type is inferred automatically. When using TypeScript, the IDE and vue-tsc infer the ref’s type from the element the ref attribute is used on. An <input> gives HTMLInputElement, a <div> gives HTMLDivElement, and a component gives the component instance type.
  • The ref is null before the mount. You can only access it in onMounted or later lifecycle hooks. Accessing it in a template expression before the mount returns null.
  • Component refs require defineExpose. A child using <script setup> is private by default. The parent can only access what the child exposes with defineExpose, and the expose must happen before any await.
  • useTemplateRef() does not work with v-for. The v-for refs still require the legacy ref([]) pattern. The array order is not guaranteed to match the source array.
  • Function refs give precise control. The :ref="(el) => setItemRef(el, item)" pattern stores the element by its associated data, not by index. This is the fix for the v-for order problem.
  • Clean up third-party libraries. Use onUnmounted to remove the library instance and set the ref to null.

Remember: useTemplateRef() is the modern, type-safe way to obtain template refs in Vue 3.5+. It replaces the fragile naming convention with an explicit string argument, and it gives you automatic type inference from the template. The ref is populated after the mount, so access it in onMounted or later. Component refs are private by default and require defineExpose. The v-for case still needs the legacy ref([]) pattern, and the order is not guaranteed. The result is the direct DOM access when you need it, with the type safety and explicitness that the old pattern lacked.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!