skip to content

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%

answer

  1. code generated in written order
  2. each guard can stop the chain
  3. self compares target and currentTarget
  4. bubbling clicks from the dialog

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.

solid answer

~40 s

`.stop`, `.prevent` and `.self` are **runtime guards** that Vue wraps around your handler in the order you write them, and each guard runs before the next. `.prevent` calls `preventDefault()` and continues; `.self` returns early unless `event.target === event.currentTarget`. So on a modal backdrop, `@click.self.prevent="close"` checks .self first and only prevents clicks on the backdrop itself. `@click.prevent.self="close"` prevents **every** click that bubbles up to the backdrop — including clicks on links and checkboxes inside the dialog — before .self decides not to call `close`. Links stop navigating and checkboxes stop toggling, a bug that looks unrelated to the modal. The Vue guide states the rule: order matters because the code is generated in the same order.

code

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

const open = ref(true)
const agree = ref(false)
function close() {
  open.value = false
}
</script>

<template>
  <div v-if="open" class="backdrop" @click.self.prevent="close">
    <div class="dialog" role="dialog" aria-modal="true">
      <a href="/terms">Read the terms</a>
      <label><input type="checkbox" v-model="agree" /> I agree</label>
    </div>
  </div>
</template>

go deeper

for a junior

Recall that .self fires only when the element itself is clicked, and that modifiers run in the order written.

for a middle

Explain the guard chain: side effects like .prevent and .stop happen when reached, filters like .self can end the chain early.

for a senior

Recognise a backdrop's .prevent.self as the cause of dead links and unticked checkboxes inside a modal, and order filters before side effects.

for a principal

Codify modifier ordering in the component library's modal and lint rules, since the failure surfaces far from its cause.

## Modifiers compile to a chain of guards When the template compiler meets `@click.stop.prevent.self="close"`, it does not produce one big `if`. It wraps the handler in a helper (`withModifiers`) with the list of guard modifiers **in the order written**. At runtime, for each event, that wrapper walks the list: 1. `.stop` calls `event.stopPropagation()` and moves on. 2. `.prevent` calls `event.preventDefault()` and moves on. 3. `.self` returns early — the handler never runs — if `event.target !== event.currentTarget`. 4. If no guard returned early, your handler runs. `.stop` and `.prevent` are **side effects that always happen** when reached; `.self` (like the system-key guards `.ctrl`, `.shift`, `.alt`, `.meta`, `.exact`, and the mouse-button guards) is a **filter** that can cut the chain. That is why position matters: a side effect written before a filter happens even for events the filter then rejects. The Vue guide puts it directly: order matters when using modifiers because the relevant code is generated in the same order. ## The modal backdrop A modal is usually a full-screen backdrop with a dialog inside it. Clicking the dimmed backdrop should close the modal; clicking inside the dialog should not. `.self` is the tool: a click that starts inside the dialog bubbles up to the backdrop with `event.target` set to the inner element, so `target !== currentTarget` and `.self` rejects it. ```vue <template> <div class="backdrop" @click.self="close"> <div class="dialog" role="dialog"> <a href="/terms">Read the terms</a> <label><input type="checkbox" v-model="agree" /> I agree</label> </div> </div> </template> ``` Now suppose someone adds `.prevent` to stop a stray default action on the backdrop. | Binding | Click on the backdrop | Click on the link inside the dialog | |---|---|---| | `@click.self="close"` | closes | navigates normally, modal stays | | `@click.self.prevent="close"` | default prevented, closes | navigates normally, modal stays | | `@click.prevent.self="close"` | default prevented, closes | **default prevented** — link does not navigate, modal stays | With `.prevent.self`, the `.prevent` guard runs for every click that bubbles through the backdrop, before `.self` rejects the inner ones. The consequences inside the dialog: - **Links do not navigate**, because their click default is cancelled. - **Checkboxes and radios do not change**, because a cancelled click reverts their toggle — the `v-model` never sees a change. - **Submit buttons do not submit** the form. The bug report reads "the terms link is broken" or "the checkbox won't tick", and nobody suspects the backdrop. ## Other pairs where order matters - `.stop.self` vs `.self.stop`: the first stops propagation of **every** bubbling click, so a document-level "click outside" listener elsewhere never hears clicks from inside the dialog; the second stops only clicks on the element itself. - `.prevent.ctrl` vs `.ctrl.prevent`: the first cancels the default for every click, with or without Ctrl; the second only when Ctrl is held. - Filters in any order among themselves (`.self.ctrl` and `.ctrl.self`) give the same result, because each only decides whether to continue. ## Rules of thumb - Put **filters first** and **side effects after** unless you really mean to apply the side effect to every event: `.self.prevent`, `.ctrl.exact.prevent`. - A modifier with no handler is valid: `@submit.prevent` just cancels the default. - `.capture`, `.once` and `.passive` are **not** part of this chain; they become options on the native listener, so their position among the guards does not matter.

  • In Vue 3, does the position of .once, .capture or .passive in a modifier chain matter?
    No. Those three are not runtime guards; the compiler turns them into options on the native listener. The guard modifiers — `.stop`, `.prevent`, `.self`, the system keys, `.exact` and mouse buttons — are the ones that run as an ordered chain around the handler; key filters such as `.enter` run in a separate key check.
  • In Vue 3, what does @click.stop.self on a backdrop break elsewhere on the page?
    `.stop` runs before `.self`, so propagation stops for every click that reaches the backdrop, including clicks inside the dialog. Any ancestor or document-level listener — a dropdown's outside-click closer, an analytics listener — never hears those clicks. `.self.stop` would stop only clicks on the backdrop itself.

Written modifiers are a line of checkpoints a traveller walks through in order. A checkpoint that stamps every passport placed before the one that turns away the wrong travellers stamps the rejected travellers too; swap the two and only admitted travellers get stamped.

saying these in an interview costs you the question

  • v-on modifiers are a set, so their order never changes behaviour.
  • .self stops the event from reaching child elements.
  • @click.prevent.self only prevents clicks on the element itself.
  • .prevent is skipped automatically when .self rejects the event.
  • The position of .capture among the other modifiers changes the result.