skip to content

Event Listeners & Modifiers

v-on attaches DOM listeners with method or inline handlers, $event, and modifiers for propagation, keys and mouse buttons. Interviewers probe modifier order and .exact key matching.

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

explore

questions

5

In Vue 3, what is the difference between a method handler and an inline handler in v-on, and how does each receive the native event?

level: juniorimportance: must knowfreq 72%

answer

  1. the compiler looks at the expression's shape
  2. identifier or property path
  3. a call wraps it in a function
  4. the special $event variable

basics

~20 s

Vue's template compiler treats a v-on value that is a name or property path (save, form.save) as a method handler, called with the native event. Anything else (save('draft'), count++) is an inline handler, where the event is available as $event.

solid answer

~40 s

`v-on` (shorthand `@`) accepts either form, and the **template compiler decides which one you wrote** from the expression's shape. If the value is a valid identifier or property access path — `onSubmit`, `form.submit`, `handlers['submit']` — it is a **method handler**: Vue calls it with the native DOM `Event` as the first argument. An arrow or function expression is passed through the same way. Anything else — `submit('draft')`, `count++` — is an **inline handler**: Vue wraps the statement in a function whose parameter is `$event`, so you pass the event on with `submit('draft', $event)`, or write `(e) => submit('draft', e)`. A classic slip is `@click="save()"` when the method expects the event: the parentheses make it inline, and the event is never passed.

code

vue · 18 lines
vue
<script setup lang="ts">
import { ref } from 'vue'

const clicks = ref(0)
function logTarget(event: MouseEvent) {
  console.log((event.target as HTMLElement).tagName)
}
function track(label: string, event: MouseEvent) {
  console.log(label, event.clientX, event.clientY)
}
</script>

<template>
  <button @click="logTarget">method handler</button>
  <button @click="clicks++">inline statement: {{ clicks }}</button>
  <button @click="track('cta', $event)">inline with $event</button>
  <button @click="(e) => track('cta', e)">inline arrow</button>
</template>

go deeper

for a junior

Recall the two forms: a bare name gets the event, a call expression needs $event or an arrow parameter to pass it on.

for a middle

Explain that the compiler classifies the value by its shape, and what wrapper it generates for inline statements.

for a senior

Spot the save() versus save bug in review, and explain why handler caching and the single invoker make inline arrows cheap.

for a principal

Set a team convention for handler style and for keeping DOM chores in modifiers, so methods stay testable data logic.

## Two ways to write a v-on value `v-on:click="…"` (shorthand `@click="…"`) attaches a native DOM listener to an element. The value between the quotes can take two forms, and Vue treats them differently: - A **method handler** names a function: `@click="close"`. - An **inline handler** is a JavaScript statement that runs when the event fires: `@click="count++"` or `@click="close('backdrop')"`. The distinction is made **at compile time**. The Vue guide spells out the rule: the compiler checks whether the value string is a valid JavaScript identifier or property access path. `foo`, `foo.bar` and `foo['bar']` are method handlers; `foo()` and `count++` are inline handlers. ## What the compiler produces | You write | Kind | What runs on the event | Event object | |---|---|---|---| | `@click="close"` | method | `close(event)` | first argument | | `@click="modal.close"` | method | `modal.close(event)` | first argument | | `@click="(e) => close('x', e)"` | function expression | the arrow, called with the event | `e` | | `@click="close('x')"` | inline | `$event => (close('x'))` | `$event`, unused here | | `@click="close('x', $event)"` | inline | `$event => (close('x', $event))` | passed on explicitly | | `@click="count++"` | inline | `$event => (count++)` | available as `$event` | Inside an inline handler, `$event` is simply the parameter name of the function Vue generates around your statement. It exists only in inline handlers; in a method handler you receive the event as a normal argument. ## A worked example A modal with a "Save as draft" and a "Publish" button, both routed to one function that needs the event. ```vue <script setup lang="ts"> function submit(mode: 'draft' | 'publish', event?: Event) { event?.preventDefault() console.log('submit as', mode) } </script> <template> <form @submit="(e) => submit('publish', e)"> <button type="submit">Publish</button> <button type="button" @click="submit('draft', $event)">Save as draft</button> </form> </template> ``` Both styles reach the same function with a mode and the event. The arrow form reads better when a handler needs a name for the event; `$event` is shorter in a one-liner. ## Mistakes interviewers listen for 1. **Calling instead of naming.** `@click="save()"` compiles to an inline handler, so `save` is called with **no** arguments. If `save(event)` expects the event, it gets `undefined`. Either drop the parentheses or pass `$event`. 2. **Expecting `$event` in a method.** In `function save($event) {}` the name is just a parameter name; there is nothing special about it outside a template inline handler. 3. **Doing DOM chores in data methods.** Writing `event.preventDefault()` or `event.stopPropagation()` inside every method mixes DOM details into data logic. The guide's answer is event modifiers — `@submit.prevent`, `@click.stop` — which keep methods about data. 4. **Assuming `this` matters.** In a `<script setup>` component handlers are plain functions from the setup scope; there is no `this`-binding concern of the kind `methods` in the Options API have. ## What happens at runtime A few details sit under the syntax: - For each event prop on an element (`onClick`, `onClickOnce`, …), Vue attaches **one** native listener (an "invoker") and on re-render only swaps the function it calls, instead of removing and re-adding listeners. - In compiled single-file components, inline handlers are usually **cached** by the compiler, so a new arrow is not created on every render; this also keeps child components receiving the same handler from re-rendering needlessly. - Errors thrown inside a native event handler are routed through Vue's error handling, so they reach `onErrorCaptured` hooks and `app.config.errorHandler` like other component errors. None of this changes how you write a handler, but it explains why inline arrows in templates are not the performance problem they are sometimes assumed to be.

  • In Vue 3, why does @click="save()" leave a save(event) method with an undefined event?
    The parentheses make the value an inline statement, so Vue wraps it as `$event => (save())` and calls `save` with no arguments. Only a bare name or property path is treated as a method handler that receives the event. Write `@click="save"` or `@click="save($event)"`.
  • Does an inline arrow handler in a Vue 3 single-file component create a new function on every render?
    Usually not. The SFC template compiler caches inline handlers, so the same function is reused across renders unless it references loop or slot scope variables. Vue also keeps one native listener per event and only swaps the function it calls, so re-renders do not re-register listeners.

saying these in an interview costs you the question

  • @click="save()" passes the click event to save automatically.
  • $event is available inside a method handler's body by that name.
  • Vue decides method vs inline at runtime by checking whether the value is a function.
  • Every re-render removes and re-adds the element's native listener.
  • Inline handlers cannot access the native event at all.
open as a page

In Vue 3, how do you bind a Ctrl+Enter submit shortcut on a textarea that Ctrl+Shift+Enter does not trigger, and what does .exact check?

level: middleimportance: should knowfreq 46%

basics

~10 s

Use @keydown.ctrl.enter.exact="submit". .enter matches the key, .ctrl requires Ctrl held, and .exact rejects the event if any other system modifier (Shift, Alt, Meta) is also pressed; without .exact, Ctrl+Shift+Enter would fire too.

open as a page

In Vue 3, why does the order of v-on modifiers matter, as in @click.prevent.self versus @click.self.prevent on a modal backdrop?

level: middleimportance: should knowfreq 52%

basics

~20 s

Vue runs v-on's guard modifiers in the order written. @click.prevent.self calls preventDefault on every click reaching the element, including clicks bubbling from children, then checks .self; @click.self.prevent checks .self first, so only clicks on the element itself are prevented.

open as a page

A Vue 3 modal closes through @click.self on its backdrop, but it also closes when a user drag-selects text in the dialog and releases over the backdrop — why, and how do you fix it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

When a press starts in the dialog and ends on the backdrop, browsers dispatch click to the nearest common ancestor — the backdrop — so .self passes. Record whether mousedown began on the backdrop and close only if it did.

open as a page

In Vue 3, how does the template compiler treat v-on's .once, .capture and .passive differently from .stop, .prevent, .self and the mouse-button modifiers?

level: middleimportance: nice to knowfreq 28%

basics

~10 s

Vue's compiler turns .once, .capture and .passive into addEventListener options, while .stop, .prevent, .self, system keys and mouse buttons become runtime guards wrapped around the handler. @click.right listens to contextmenu and @click.middle to mouseup.

open as a page