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?
answer
- listener options vs runtime guards
- a suffix on the event prop name
- click.right is not a click
- passive promises no preventDefault
basics
~10 sVue'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 sThe 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<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
Recall what .once, .capture and .passive do, and that .right and .middle restrict mouse buttons.
Explain the split between listener options appended to the event name and runtime guards wrapped around the handler.
Know the rewrites — click.right to contextmenu, click.middle to mouseup — and never pair .passive with .prevent.
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.