Vue.js: Refs & DOM
Last updated: 2026-10-20
Template refs let you access DOM elements or child component instances directly—for example calling input.focus() or invoking a component method. Vue 3.5 introduced the more capable useTemplateRef composition API, with TypeScript type inference to match.
Template refs are an escape hatch—Vue recommends solving most problems with ref/reactive/props/emit. Reach for template refs only when you need to manipulate the DOM or a component instance directly.
1. What You'll Learn
- The basics of
ref="el"template refs useTemplateRef(recommended in Vue 3.5+)- Accessing DOM elements (focus, scrollIntoView, etc.)
- Accessing child component instances (exposing methods with defineExpose)
- Replacing
$refswith the Composition API - Ref arrays inside v-for
- 5 real-world scenarios and 4 anti-patterns
2. The "Auto-Focus" Headache of a Login Form
(1) The Pain: How Do I Auto-Focus the Input After the Modal Opens?
Alice built a login modal that needs to focus the username input automatically:
// ❌ Broken version: querySelector directly
onMounted(() => {
const input = document.querySelector('.username-input')
input.focus() // ❌ Not the Vue way
})
The Vue philosophy: avoid manipulating the DOM directly. Use template refs instead.
Product manager Charlie:
"Alice, when the modal opens, the username field should be focused automatically so the user can start typing right away."
(2) The Vue Template Ref Fix
(3) The Payoff
After adopting template refs:
- Readability: it's explicit that "I want a ref to this element"
- Vue philosophy: no direct DOM manipulation
- Type safety: TypeScript infers the element type
- Correct lifecycle: access happens in onMounted or nextTick
3. Basic ref Usage
(1) String refs (Vue 2 style, deprecated)
// ❌ Not recommended: string ref
export default {
mounted() {
this.$refs.input.focus()
}
}
(2) Variable refs (recommended in Vue 3)
(3) 5 Basic Operations
// 1. Access the DOM element
inputRef.value // the <input> element
// 2. Call DOM APIs
inputRef.value.focus()
inputRef.value.blur()
inputRef.value.select()
inputRef.value.scrollIntoView()
// 3. Read/modify DOM properties
inputRef.value.value // the input's value
inputRef.value.disabled // the disabled property
inputRef.value.style.color = 'red' // change styles
// 4. Listen to DOM events (not recommended, use @event)
inputRef.value.addEventListener('focus', handler)
// 5. Access a child component instance (see 17.4)
childRef.value.someMethod()
▶ Example: Auto-focusing a login form
▶ Try it YourselfOutput:
The page renders a form with Username and Password inputs plus a green Login button.
As soon as it loads, the caret sits in the Username field (its border shows focus), and a grey hint below reads Focus jumps to the Username field as soon as the page loads.
Submitting logs Login: <username> <password> to the console.
[ Username ] ← focused as soon as the page loads
[ Password ]
[ Login ]
Focus jumps to the Username field as soon as the page loads
# on submit (console)
Login: alice 123456
▶ Example: Auto-scrolling to the bottom (chat box)
▶ Try it YourselfOutput:
A 150px-tall scrollable box shows three seeded messages, with an input and a Send button beneath it.
Typing a message and pressing Enter (or clicking Send) appends it to the list, clears the input, and scrolls the box to the newest message automatically.
┌────────────────────────────────────┐
│ 👋 Welcome to the chat room │
│ This is history message 2 │
│ This is history message 3 │
│ Hello │ ← just sent, scrolled into view
└────────────────────────────────────┘
[ Type a message... ] [ Send ]
4. useTemplateRef (Recommended in Vue 3.5+)
(1) Why Do We Need useTemplateRef?
With variable refs in <script setup> you need 2 names (the template ref and the variable), and it's easy to let them drift apart. useTemplateRef uses a single name and gives you stronger TypeScript inference.
(2) 5 Advantages
| Advantage | Description |
|---|---|
| Type inference | TypeScript knows it's an HTMLInputElement |
| Rename safety | Renaming updates everywhere in the IDE |
| No name mismatch | A template ref that doesn't match the variable errors out |
| Simpler setup | No const inputRef = ref(null) needed |
| Better DevTools | Supported by Vue DevTools 5.x |
(3) Side-by-Side Comparison
<!-- Old style: variable ref -->
<template>
<input ref="usernameInput">
</template>
<script setup>
const { ref, onMounted } = Vue
const inputRef = ref(null) // the name may not match the template
onMounted(() => inputRef.value.focus())
</script>
<!-- New style: useTemplateRef (recommended in Vue 3.5+) -->
<template>
<input ref="usernameInput">
</template>
<script setup>
const { useTemplateRef, onMounted } = Vue
const inputRef = useTemplateRef('usernameInput') // one name does it all
onMounted(() => inputRef.value.focus())
</script>
5. Accessing Child Component Instances
(1) Exposing Methods with defineExpose
(2) Access from the Parent
<template>
<MyInput ref="myInputRef" v-model="searchQuery" />
<button @click="focusInput">Focus Input</button>
</template>
<script setup>
const { ref } = Vue
import MyInput from './MyInput.vue'
const searchQuery = ref('')
const myInputRef = ref(null)
function focusInput() {
myInputRef.value.focus() // call the method exposed by the child
myInputRef.value.select() // chaining also works
}
</script>
(3) 5 Common Use Cases
| Scenario | Child exposes | Parent calls |
|---|---|---|
| Focus a field | focus() |
inputRef.focus() |
| Clear a form | clear() |
formRef.clear() |
| Reload data | reload() |
tableRef.reload() |
| Open a modal | open() |
modalRef.open() |
| Submit a form | submit() |
formRef.submit() |
6. Ref Arrays in v-for
(1) Basic Usage
(2) Dynamic refs (v-for with a dynamic count)
(3) 5 Things to Watch Out For
| Point | Description |
|---|---|
| Array order | Matches the order of the v-for data |
| Reactivity | Changes to the ref array need a watch |
| Conditional rendering | Refs may not update after v-if |
| Dynamic count | Use an object or function ref |
| Performance | Many refs (100+) slow rendering down |
7. Putting It Together: Parent-Child ref Validation
▶ Example: Parent calling a child's method (defineExpose)
▶ Expand runnable example (69 lines)
Output:
The child renders an Email field and a Password field, followed by a green Submit button.
Clicking Submit with an empty email shows a red ❌ Invalid email and moves focus to the email field.
With a valid-looking email but a password shorter than 6 characters you get ❌ Invalid password with focus in the password field;
a fully valid form shows the green ✅ Validation passed!.
[ Email ]
[ Password (min 6 chars) ]
[ Submit ]
empty email → ❌ Invalid email (focus moves to Email)
short password → ❌ Invalid password (focus moves to Password)
valid form → ✅ Validation passed!
(1) 📊 5 Common Mistakes at a Glance
| Mistake | Symptom | Fix |
|---|---|---|
| Misspelled template ref | ref.value is null | Make sure the names match |
| Accessing at the top level of setup | ref.value is null | Use onMounted |
| Accessing inside a modal | Doesn't work | Wait for the DOM with nextTick |
| Accessing a v-for ref array | Indexes are off | Keep the order stable with :key |
| Cross-component ref | undefined | Expose with defineExpose in the child |
(2) 📊 5 Types of Refs, Compared
| Type | Example | Best for |
|---|---|---|
| DOM element | ref="inputRef" → HTMLInputElement |
Accessing input/div |
| Component instance | ref="childRef" → component instance |
Calling child methods |
| v-for array | ref="itemRefs" → Array |
Accessing list items |
| Function ref | :ref="el => ..." → a single element |
Dynamic counts |
| String ref | ref="name" → this.$refs |
Vue 2 style (not recommended) |
❓ FAQ
useTemplateRef<HTMLInputElement>('usernameInput'). Or ref<HTMLInputElement | null>(null).📖 Summary
- Template refs give direct access to DOM elements or child component instances
- 3 styles: variable ref (Vue 3) / useTemplateRef (recommended in Vue 3.5+) / string ref (Vue 2 style, not recommended)
- 5 basic operations: focus / scroll / select / change properties / call child methods
- defineExpose lets a child expose methods to its parent
- Refs inside v-for are collected into an array automatically
- 5 scenarios: auto-focus / scroll to bottom / call child methods / third-party integration / measure element size
- 4 anti-patterns: typos / top-level access / modal without nextTick / cross-component refs
📝 Exercises
-
Basic (difficulty ⭐) Build a simple auto-focus form:
- 1 input + 1 button
- The input is focused automatically when the page loads
- Clicking the button changes its label to "Submitted" and disables it
-
Intermediate (difficulty ⭐⭐) Build a chat box that scrolls to the bottom automatically:
- A message list (div container)
- An input + send button
- After sending, the message is appended and the list scrolls to the bottom
- Render messages with v-for and manage their DOM with a ref array
-
Challenge (difficulty ⭐⭐⭐) Build a complete "parent-child + template refs" system:
- FormValidator child: exposes validate() and reset()
- LoginPage parent: calls validate to check the form and shows errors on failure
- 5 input fields (username/email/password/phone/captcha)
- useTemplateRef (Vue 3.5+) + strong TypeScript types
- Focus the first input automatically when the modal opens
- On failure, focus the first invalid field