skip to content

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%

answer

  1. listener options vs runtime guards
  2. a suffix on the event prop name
  3. click.right is not a click
  4. passive promises no preventDefault

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.

solid answer

~40 s

The compiler sorts v-on modifiers into three groups. **Listener options** — `.capture`, `.once`, `.passive` — are appended to the event prop name (`onClickOnce`, `onScrollPassive`) and the runtime passes them to `addEventListener`, so the browser enforces them and their position does not matter. **Runtime guards** — `.stop`, `.prevent`, `.self`, `.ctrl`/`.shift`/`.alt`/`.meta`, `.exact`, `.left`/`.middle`/`.right` on mouse events — wrap the handler in the written order. **Key modifiers** wrap it in a separate key check. Two rewrites surprise people: `@click.right` actually listens to `contextmenu` and `@click.middle` to `mouseup`, because a secondary or auxiliary press does not fire `click`. And `.passive` must not be combined with `.prevent`: a passive listener has promised not to cancel the default, so the prevent is ignored and browsers usually warn.

code

vue · 17 lines
vue
<script setup lang="ts">
function openMenu(e: MouseEvent) {
  console.log('custom menu at', e.clientX, e.clientY)
}
function onScroll() {
  console.log('scrolled')
}
function onFirstFocus() {
  console.log('first focus only')
}
</script>

<template>
  <div @click.right.prevent="openMenu">right-click me</div>
  <div class="feed" @scroll.passive="onScroll">...</div>
  <input @focus.once="onFirstFocus" />
</template>

go deeper

for a junior

Recall what .once, .capture and .passive do, and that .right and .middle restrict mouse buttons.

for a middle

Explain the split between listener options appended to the event name and runtime guards wrapped around the handler.

for a senior

Know the rewrites — click.right to contextmenu, click.middle to mouseup — and never pair .passive with .prevent.

for a principal

Judge where scroll and touch listeners must be passive across the app, and where a deliberate non-passive listener is justified.

## Three kinds of modifier `v-on` modifiers look alike in a template, but Vue 3's DOM compiler splits them into three groups that are implemented in completely different places. | Group | Modifiers | Implemented as | Does order matter? | |---|---|---|---| | **Listener options** | `.capture`, `.once`, `.passive` | options passed to the browser's `addEventListener` | no | | **Guards** | `.stop`, `.prevent`, `.self`, `.ctrl`, `.shift`, `.alt`, `.meta`, `.exact`, `.left`/`.middle`/`.right` (mouse) | a runtime wrapper (`withModifiers`) around the handler | yes | | **Key filters** | `.enter`, `.esc`, `.page-down`, `.left`/`.right` (keyboard), … | a runtime wrapper (`withKeys`) that checks `event.key` | only among themselves | ## Listener options: handled by the browser For `@scroll.passive="onScroll"` the compiler does not wrap the handler. It changes the **prop name**: the vnode gets `onScrollPassive`, and `@click.once.capture` becomes something like `onClickOnceCapture`. When the element is patched, the runtime strips the `Once`, `Passive` and `Capture` suffixes from the name, turns them into an options object, and calls `addEventListener('scroll', invoker, { passive: true })`. Consequences: - The browser, not Vue, enforces them: capture-phase registration, automatic removal after one call, or the passive promise. - Their position among the modifiers is irrelevant; `.once.stop` and `.stop.once` behave the same. - `@click` and `@click.once` on the same element are two different props, so they are two different native listeners. ## Guards: Vue code that runs before your handler Guards are Vue functions executed in the order written. `.stop` and `.prevent` call `stopPropagation()` / `preventDefault()`; the others return early and skip the handler: `.self` when `target !== currentTarget`, `.ctrl` when Ctrl is not held, `.exact` when an unlisted system key is held, `.left` when the pressed button is not the main one. ## The mouse-button rewrite Browsers fire `click` for the main button. A secondary (right) press produces a `contextmenu` event, and an auxiliary (middle) press does not produce `click` either. Vue compensates at compile time: 1. `@click.right` is rewritten to listen to **`contextmenu`**, with a guard that checks the button. 2. `@click.middle` is rewritten to listen to **`mouseup`**, with a guard for the middle button. 3. `@click.left` stays a `click` listener with a main-button guard. So `@click.right.prevent="openMenu"` is the idiomatic way to replace the browser's context menu with your own: it is really a `contextmenu` listener that cancels the default. The Vue guide adds that `.left`, `.right` and `.middle` mean the **main, secondary and auxiliary** pointer buttons, not physical positions: a left-handed mouse or a two-finger trackpad tap maps accordingly. On keyboard events (`keydown`, `keyup`, `keypress`) the compiler treats `.left` and `.right` as **arrow-key** filters instead. ## Why .passive and .prevent do not mix `.passive` tells the browser the listener will never call `preventDefault()`, which lets it start scrolling without waiting for the handler. If you also write `.prevent`, Vue's guard still calls `preventDefault()`, but on a passive listener that call is ignored and browsers typically log a warning. The Vue guide says it plainly: do not use `.passive` and `.prevent` together. - For scroll or touch listeners that only observe, use `.passive`. - For listeners that must cancel scrolling (a custom drag), omit `.passive` and call `preventDefault()` deliberately. ## Summary - **Options** change how the native listener is registered. - **Guards** change whether and how your handler runs, in the written order. - **Mouse-button modifiers** can change which event is listened to at all.

  • In Vue 3, which native event does @click.middle actually listen to?
    `mouseup`. A middle-button press does not fire `click` in browsers, so the compiler rewrites the event name to `mouseup` and adds a guard that lets the handler run only for the auxiliary button.
  • In Vue 3, does @keydown.left filter a mouse button?
    No. On keyboard event names — `keydown`, `keyup`, `keypress` — the compiler treats `.left` and `.right` as arrow-key filters. They mean mouse buttons only on mouse events such as `click` or `mouseup`.

saying these in an interview costs you the question

  • @click.right listens to click and checks the button inside the handler.
  • .once is a Vue guard that counts calls, so its position in the chain matters.
  • .passive.prevent lets you cancel scrolling while keeping passive performance.
  • .left always means the physical left mouse button.
  • All v-on modifiers are implemented as runtime wrappers around the handler.