skip to content

Custom DOM Directives

Custom directives hook into an element's mount, update and unmount to wrap low-level DOM access, via a hook object or function shorthand. Interviewers ask when a directive beats a component.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Vue 3 `<script setup>` component, how do you define and use a `v-focus` directive, and which hook should do the focusing?

level: juniorimportance: should knowfreq 55%

answer

  1. v-prefixed camelCase variable
  2. object of hooks
  3. element must be in the DOM
  4. mounted, not created
  5. shorthand also runs on update

basics

~20 s

Declare const vFocus = { mounted: (el) => el.focus() } in script setup and write v-focus on the element. Use mounted because the element is in the document only then; the function shorthand would refocus on every update.

solid answer

~40 s

In `<script setup>`, any camelCase variable starting with `v` can be used as a directive, so `const vFocus = { mounted: (el) => el.focus() }` enables `<input v-focus />` with no extra registration. A directive is an object of optional hooks named like component hooks, each receiving the element. Focusing needs the element to be in the document, which is guaranteed in `mounted`; `created` runs before attributes and listeners are applied and `beforeMount` before insertion. Avoid the function shorthand (`const vFocus = (el) => el.focus()`): a function directive runs in both `mounted` and `updated`, so the input would steal focus every time its component re-renders. Unlike the `autofocus` attribute, the directive also works when Vue inserts the element later, such as under a `v-if`.

code

vue · 16 lines
vue
<script setup>
import { ref } from 'vue'

const editing = ref(false)

const vFocus = {
  mounted(el, binding) {
    if (binding.value !== false) el.focus()
  }
}
</script>

<template>
  <button @click="editing = true">Rename</button>
  <input v-if="editing" v-focus />
</template>

go deeper

for a junior

Remember the vFocus naming rule and that mounted is where the element can be focused.

for a middle

Explain the hook order and why the function shorthand maps to both mounted and updated.

for a senior

Judge when a shared directive beats a template ref in each component, and avoid focus-stealing on re-render.

for a principal

Decide which small DOM behaviours a team standardises as shared directives and how they are documented.

## What a custom directive is Vue ships built-in directives such as `v-model` and `v-show`. A **custom directive** is your own reusable piece of low-level DOM logic attached to an element. It is defined as an object whose optional **hooks** mirror the component lifecycle and receive the element as their first argument. The Vue guide describes them as intended for "reusing logic that involves low-level DOM access on plain elements", and cites `v-focus` as the standard example. ## Defining and using v-focus in script setup ```vue <script setup> const vFocus = { mounted: (el) => el.focus() } </script> <template> <input v-focus placeholder="Search" /> </template> ``` The naming rule does the registration: - In `<script setup>`, any **camelCase variable that starts with the `v` prefix** can be used as a directive. - `vFocus` becomes `v-focus` in the template; `vClickOutside` becomes `v-click-outside`. - An imported directive works the same way: `import { vFocus } from './directives'`. - Without `<script setup>`, you would list it in the `directives` option; app-wide registration goes through `app.directive`. ## Choosing the hook A directive object may define seven hooks. For focusing, the question is when the element is actually in the document: | Hook | When it runs | Can it focus? | |---|---|---| | `created` | before the element's attributes and event listeners are applied | no: not inserted yet | | `beforeMount` | right before the element is inserted into the DOM | no: not inserted yet | | `mounted` | after the owning component and its children are mounted | yes | | `updated` | after the owning component and its children update | yes, but repeatedly | `mounted` is the right hook: it runs once per element insertion, after the element is in the document. ## The function shorthand trap Vue lets you define a directive as a single function. That function is used for **both `mounted` and `updated`**: ```js // looks tidy, but refocuses on every re-render const vFocus = (el) => el.focus() ``` `updated` runs whenever the component that owns the template re-renders, not only when the directive's value changes. So typing in another field of the same form, which updates state and re-renders, would yank focus back to this input. The shorthand suits directives that should re-apply on every update, such as setting a colour from `binding.value`, not one-time actions. ## Why not the autofocus attribute The HTML `autofocus` attribute acts when the page loads. Elements that Vue inserts later, for example a search field revealed by `v-if` or an input inside a dialog opened by a click, do not get focus from it. The directive's `mounted` hook runs whenever Vue inserts the element, so it covers those cases. ## Variations worth knowing 1. **Conditional focus:** pass a value, `v-focus="isEditing"`, and focus only when `binding.value` is true. 2. **Refocus on a change:** add an `updated` hook that compares `binding.value` with `binding.oldValue` and focuses only when it flips to true. 3. **Selecting text:** call `el.select()` after focusing for inline-edit fields. ## Common mistakes - Naming the variable `focus` or `VFocusDirective` and wondering why `v-focus` is unknown. - Using `created` or `beforeMount`, where `focus()` does nothing because the element is not in the document. - Using the function shorthand for a one-time action. - Using a directive where a template ref plus `onMounted` in the same component is simpler; a directive pays off when many components need the same behaviour. ## The full hook order at a glance For one element carrying a directive, the hooks run in this order over its life: 1. `created` — the element exists but its attributes and listeners are not applied yet. 2. `beforeMount` — attributes are applied; the element is about to be inserted. 3. `mounted` — after the owning component and all its children are mounted. 4. `beforeUpdate` and `updated` — once per re-render of the owning component, for as long as the element stays. 5. `beforeUnmount` and `unmounted` — when the element is removed, for example by `v-if`. Step 4 is tied to the **owner's** re-renders, not to the directive's value, which is exactly why a function shorthand repeats its work.

  • In Vue 3, why does `const vFocus = (el) => el.focus()` make typing elsewhere in the same form jump back to that input?
    A function directive is registered for both `mounted` and `updated`. `updated` runs whenever the component owning the template re-renders, and typing in another bound field re-renders it, so the function runs again and moves focus. Use an object with only `mounted` for a one-time action.
  • How would you make a Vue 3 `v-focus` directive focus again whenever its bound value turns true?
    Add an `updated` hook that checks `binding.value && !binding.oldValue` and then calls `el.focus()`. `oldValue` is available in `beforeUpdate` and `updated`, whether or not the value changed, so the comparison is what limits focusing to the moment the value flips to true.

A v-focus directive is like a note on a desk that says 'switch me on when installed': mounted reads it once at installation, while the function shorthand rereads it at every office tidy-up.

saying these in an interview costs you the question

  • Focuses in created or beforeMount before the element is inserted
  • Uses the function shorthand for a one-time focus
  • Names the variable focus and expects v-focus to resolve
  • Thinks updated runs only when the directive's own value changes
  • Assumes the autofocus attribute handles elements Vue inserts later
open as a page

In Vue 3, what does a custom directive hook receive for `v-tooltip:top.delay="hint"`, and when is `binding.oldValue` available?

level: middleimportance: should knowfreq 45%

basics

~20 s

Each hook gets el, binding, vnode and, in beforeUpdate and updated, prevVnode. For v-tooltip:top.delay="hint" the binding has value = hint's value, arg = 'top', modifiers = { delay: true }, plus instance and dir; oldValue is set only in beforeUpdate and updated.

open as a page

A Vue 3 `v-click-outside` directive leaks document listeners and sometimes calls an outdated handler; how should its hooks be written to fix both?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Register one document listener in mounted and store it keyed by the element, update the stored callback in updated when binding.value changes, and remove the listener in unmounted. Leaks come from adding in updated or never removing; stale calls come from capturing binding.value once.

open as a page

When should a Vue 3 codebase implement a behaviour as a custom directive rather than a component, a composable or a built-in binding?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Use a custom directive only when the behaviour needs direct DOM manipulation on a plain element, such as focusing or detecting outside clicks. Use a component when it renders markup, a composable for stateful logic, and built-in bindings whenever they can express it.

open as a page

In Vue 3, what happens when you apply a custom directive to a component instead of a plain element, and why is it discouraged?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

The directive is applied to the component's root element, much like fallthrough attributes. With several root nodes it is ignored and a development warning appears, and unlike attributes it cannot be redirected with v-bind="$attrs", so the docs advise against it.

open as a page