| |

Vue.js 11 🟢 Event Modifiers (.prevent, .stop, .capture, .self, .once, .passive)

The previous chapter covered event handling with v-on and the @ shorthand. It introduced modifiers briefly—.stop, .prevent, .once, and others—as suffixes that change how a handler responds to an event. This chapter takes the modifier system in full. Modifiers are not sugar. They are a declarative interface to the DOM event model, and they replace the imperative calls that would otherwise clutter every handler: event.stopPropagation(), event.preventDefault(), capture-phase registration, passive scroll listeners, and the one-time handler pattern. Understanding each modifier means understanding the underlying DOM behavior, because the modifier is a thin wrapper around it.

This chapter covers every event modifier Vue provides. You will learn what each one does at the DOM level, when to reach for it, and how modifiers chain. You will also see the interaction between .passive and .prevent, the role of .exact in system modifier combinations, and the difference between .capture on the element and capture-phase propagation through ancestors.

Key point: Event modifiers are applied in the order they are written, left to right. @click.stop.prevent calls stopPropagation() first, then preventDefault(). Reversing them produces the same behavior in this case, but for modifiers with side effects, order can matter. The modifier chain is compiled into a single handler that applies each modifier in sequence before invoking your method.


Why event modifiers exist

The boilerplate problem. Every non-trivial event handler eventually needs to call event.preventDefault() or event.stopPropagation(). Writing these calls inline is repetitive, and forgetting one is a common bug. The .prevent and .stop modifiers express the same intent without the imperative call, and their presence in the template is visible at a glance.

The registration-phase problem. Event listeners can be registered in two phases: bubbling and capture. The default is bubbling. To use capture, you call addEventListener with { capture: true }. The .capture modifier provides the same option declaratively. The difference matters when an ancestor needs to intercept an event before a descendant handles it.

The target-check problem. A click on a button inside a modal often bubbles to the modal’s backdrop, where a click-outside-to-close handler fires. The .self modifier prevents this by only firing the handler when the event target is the element itself, not a descendant. Without .self, the handler would need to compare event.target to event.currentTarget, a pattern that is easy to get wrong.

The one-time problem. Some handlers should fire only once: an initialization, a first-click, a discount applied on the first purchase. The .once modifier registers the listener with { once: true }, which removes it after the first invocation. Doing this manually requires a flag or a call to removeEventListener inside the handler.

The performance problem. Scroll and touch listeners that call preventDefault() block the browser’s scrolling thread and cause jank. The .passive modifier tells the browser that the handler will not call preventDefault(), allowing it to scroll immediately. This is the single most impactful performance improvement available for scroll-heavy interfaces.

The system modifier problem. Ctrl+click, Alt+click, and Shift+click are distinct interactions that often need distinct handlers. The system modifiers .ctrl, .alt, .shift, and .meta combine with key and mouse events. The .exact modifier tightens the match to exclude other system modifiers.

The key modifier problem. Keyboard interactions are commonly filtered by key: Enter to submit, Escape to close, arrow keys to navigate. The key modifiers .enter, .esc, .tab, .delete, .space, and the arrow keys provide a readable way to express these filters.


a. Event propagation modifiers

Three modifiers control how an event propagates through the DOM: .stop, .prevent, and .self.

.stop calls event.stopPropagation(), preventing the event from bubbling to ancestors. The handler still runs; only the propagation stops.

<div @click="outer">
  <button @click.stop="inner">Click</button>
</div>

Clicking the button runs inner and stops the event. The outer handler does not run. Without .stop, the event bubbles and both handlers run.

.prevent calls event.preventDefault(), preventing the browser’s default action. It does not affect propagation. A form submit that is prevented does not reload the page; a link click that is prevented does not navigate.

<form @submit.prevent="onSubmit">
  <button type="submit">Submit</button>
</form>

The handler runs, and the form does not submit. This is the standard pattern for handling form submission with JavaScript.

.self fires the handler only when event.target === event.currentTarget. It does not stop propagation. If a child is clicked, the event still bubbles past the element; the handler is simply not invoked.

<div @click.self="closeModal" class="backdrop">
  <div class="modal-content">Content</div>
</div>

Clicking the backdrop runs closeModal. Clicking inside .modal-content bubbles up to the backdrop but does not run the handler because the target is not the backdrop itself.

The three modifiers can be combined. @click.stop.prevent stops propagation and prevents the default action. @click.self.stop fires only when the target is the element itself and stops propagation when it does.


b. Phase and repetition modifiers

Three modifiers control when a listener is registered and how many times it fires: .capture, .once, and .passive.

.capture registers the listener in the capture phase instead of the bubbling phase. In the capture phase, events travel from the root toward the target. An ancestor with .capture receives the event before the target’s own handler.

<div @click.capture="onCapture">
  <button @click="onButton">Click</button>
</div>

Clicking the button runs onCapture first (capture phase), then onButton (target phase). Without .capture, the order is reversed: onButton runs first, then onCapture during bubbling.

Capture is used when an ancestor needs to intercept events before descendants handle them. Modal backdrops, global keyboard handlers, and event logging are common cases.

.once registers the listener with { once: true }, so it is removed after the first invocation.

<button @click.once="initialize">Initialize</button>

The first click runs initialize. Subsequent clicks do nothing because the listener has been removed. This is useful for initialization logic, one-time animations, and preventing double submissions.

.passive registers the listener with { passive: true }, signaling that the handler will not call preventDefault(). The browser can then scroll immediately rather than waiting for the handler to run and potentially block the scroll.

<div @scroll.passive="onScroll" class="scroll-container">
  <!-- content -->
</div>

Passive listeners are the recommended default for scroll, touchstart, and touchmove handlers, where blocking the main thread causes visible jank. The performance difference is significant on mobile devices.

A critical interaction: .passive and .prevent cannot be used together meaningfully. A passive listener cannot call preventDefault()—the browser ignores the call and logs a console warning. If .passive is present, the browser has already been told the handler will not prevent, so the default action proceeds regardless. Combining the two modifiers produces a warning and no prevention.


c. System modifiers, exact, and ordering

System modifiers filter events based on which modifier keys are held. The four are .ctrl, .alt, .shift, and .meta (Command on macOS, Windows key on Windows).

<button @click.ctrl="onCtrlClick">Ctrl+Click</button>
<button @click.alt="onAltClick">Alt+Click</button>
<button @click.shift="onShiftClick">Shift+Click</button>
<button @click.meta="onMetaClick">Cmd+Click</button>

A system modifier fires when the corresponding key is held and any others may or may not be. @click.ctrl fires when Ctrl is held, regardless of Shift or Alt. To require Ctrl and only Ctrl, add .exact.

<button @click.ctrl.exact="onCtrlOnly">
  Ctrl+Click, no other keys
</button>
<button @click.exact="onNoModifiers">
  Click with no modifier keys
</button>

The .exact modifier tightens the match to exclude all system modifiers not explicitly listed. With .exact and no system modifier, the handler fires only when no modifier key is held.

System modifiers also combine with key modifiers. @keyup.ctrl.enter fires when Ctrl and Enter are pressed together.

<input @keyup.ctrl.enter="submit">
<input @keyup.alt.enter="forceSubmit">

Modifier order matters when multiple modifiers have side effects. The chain is applied left to right:

<a @click.stop.prevent="doThis">Link</a>

The compiled handler calls stopPropagation() first, then preventDefault(), then doThis. In this case the order is indistinguishable from .prevent.stop, but for clarity and consistency, .stop before .prevent is a common convention.

For .capture, the modifier affects the registration phase, not the handler invocation. It must come before any system or key modifiers for readability:

<div @click.capture.stop="intercept">Capture and stop</div>

The element registers a capture-phase listener. The handler calls stopPropagation() and runs intercept. Descendants do not receive the event because it was stopped in the capture phase.

The .self modifier checks event.target === event.currentTarget at invocation time. It is not a registration modifier; it is a runtime check. It composes with other modifiers:

<div @click.self.stop="onBackdropClick">Backdrop</div>

The handler runs only when the target is the backdrop, and stops propagation when it does.


Complete Example Session

<!-- ============================================ -->
<!-- PART 1: BASIC .stop -->
<!-- ============================================ -->
<template>
  <div @click="outer">
    <button @click.stop="inner">Click</button>
  </div>
</template>

<script setup>
function outer() { console.log('outer') }
function inner() { console.log('inner') }
// Clicking button: logs 'inner' only
</script>
<!-- ============================================ -->
<!-- PART 2: .prevent ON FORM -->
<!-- ============================================ -->
<template>
  <form @submit.prevent="onSubmit">
    <input v-model="email">
    <button type="submit">Submit</button>
  </form>
</template>

<script setup>
import { ref } from 'vue'
const email = ref('')

function onSubmit() {
  console.log('Submitted:', email.value)
  // Page does not reload
}
</script>
<!-- ============================================ -->
<!-- PART 3: .self FOR BACKDROP -->
<!-- ============================================ -->
<template>
  <div @click.self="closeModal" class="backdrop">
    <div class="modal">Content</div>
  </div>
</template>

<script setup>
function closeModal() {
  console.log('Backdrop clicked')
  // Does not fire when clicking .modal
}
</script>
<!-- ============================================ -->
<!-- PART 4: .capture PHASE -->
<!-- ============================================ -->
<template>
  <div @click.capture="onCapture">
    <button @click="onButton">Click</button>
  </div>
</template>

<script setup>
function onCapture() { console.log('capture') }
function onButton() { console.log('button') }
// Order: 'capture', then 'button'
</script>
<!-- ============================================ -->
<!-- PART 5: .once -->
<!-- ============================================ -->
<template>
  <button @click.once="initialize">Initialize</button>
</template>

<script setup>
function initialize() {
  console.log('Initialized — runs only once')
}
</script>
<!-- ============================================ -->
<!-- PART 6: .passive FOR SCROLL -->
<!-- ============================================ -->
<template>
  <div @scroll.passive="onScroll" class="scroll-container">
    <p v-for="n in 100" :key="n">Item {{ n }}</p>
  </div>
</template>

<script setup>
function onScroll(event) {
  // Handler does not call preventDefault
  console.log(event.target.scrollTop)
}
</script>
<!-- ============================================ -->
<!-- PART 7: SYSTEM MODIFIERS -->
<!-- ============================================ -->
<template>
  <button @click.ctrl="onCtrl">Ctrl+Click</button>
  <button @click.alt="onAlt">Alt+Click</button>
  <button @click.shift="onShift">Shift+Click</button>
  <button @click.meta="onMeta">Cmd+Click</button>
</template>

<script setup>
function onCtrl() { console.log('ctrl') }
function onAlt() { console.log('alt') }
function onShift() { console.log('shift') }
function onMeta() { console.log('meta') }
</script>
<!-- ============================================ -->
<!-- PART 8: .exact -->
<!-- ============================================ -->
<template>
  <button @click.ctrl.exact="onCtrlOnly">
    Ctrl+Click (no other keys)
  </button>
  <button @click.exact="onNoModifiers">
    Click with no modifiers
  </button>
</template>

<script setup>
function onCtrlOnly() { console.log('ctrl only') }
function onNoModifiers() { console.log('no modifiers') }
</script>
<!-- ============================================ -->
<!-- PART 9: CHAINED MODIFIERS -->
<!-- ============================================ -->
<template>
  <a @click.stop.prevent="doThis" href="#">Link</a>
  <div @click.self.stop="onBackdrop">Backdrop</div>
  <div @click.capture.stop="intercept">Intercept</div>
</template>

<script setup>
function doThis() { console.log('stopped and prevented') }
function onBackdrop() { console.log('self and stop') }
function intercept() { console.log('capture and stop') }
</script>
<!-- ============================================ -->
<!-- PART 10: COMBINING WITH KEY MODIFIERS -->
<!-- ============================================ -->
<template>
  <input @keyup.enter="submit">
  <input @keyup.esc="cancel">
  <input @keyup.ctrl.enter="forceSubmit">
  <button @click.ctrl.exact="onCtrlClick">Ctrl only</button>
</template>

<script setup>
function submit() { console.log('entered') }
function cancel() { console.log('escaped') }
function forceSubmit() { console.log('ctrl+enter') }
function onCtrlClick() { console.log('ctrl click only') }
</script>

The ten parts covered .stop, .prevent, .self, .capture, .once, .passive, system modifiers, .exact, chained modifiers, and combining modifiers with key modifiers.


Quick Reference

Event Propagation Modifiers

ModifierCallsEffect
.stopstopPropagation()Prevents bubbling
.preventpreventDefault()Prevents default action
.self—Fires only if target is element
.stop.preventBothStops and prevents

Phase and Repetition Modifiers

ModifierRegisters WithEffect
.capture{ capture: true }Capture-phase listener
.once{ once: true }Removed after first call
.passive{ passive: true }Signals no preventDefault

System Modifiers

ModifierKey
.ctrlControl
.altAlt
.shiftShift
.metaCommand (Mac) / Windows
.exactExcludes other system modifiers

Key Modifiers

ModifierKey
.enterEnter
.tabTab
.deleteDelete or Backspace
.escEscape
.spaceSpace
.upArrow up
.downArrow down
.leftArrow left
.rightArrow right

Mouse Button Modifiers

ModifierButton
.leftLeft button
.rightRight button
.middleMiddle button

Best Practices

✅ Do This:

<!-- Use .prevent for form submission -->
<form @submit.prevent="onSubmit">                              <!-- ✅ -->

<!-- Use .stop to prevent bubbling -->
<button @click.stop="inner">Click</button>                     <!-- ✅ -->

<!-- Use .self for backdrop clicks -->
<div @click.self="closeModal">Content</div>                    <!-- ✅ -->

<!-- Use .once for initialization -->
<button @click.once="init">Start</button>                      <!-- ✅ -->

<!-- Use .passive for scroll -->
<div @scroll.passive="onScroll">                               <!-- ✅ -->

<!-- Use .exact for precise modifier matching -->
<button @click.ctrl.exact="onCtrlOnly">                        <!-- ✅ -->

<!-- Chain modifiers in a consistent order -->
<a @click.stop.prevent="doThis">                               <!-- ✅ -->

❌ Don’t Do This:

<!-- Don't combine .passive with .prevent -->
<div @scroll.passive.prevent="onScroll">                       <!-- ❌ -->

<!-- Don't call event methods when modifiers exist -->
<a @click="(e) => e.preventDefault()">                         <!-- ❌ -->
<a @click.prevent>                                              <!-- ✅ -->

<!-- Don't use .capture without understanding phase order -->
<div @click.capture="onCapture">                               <!-- ⚠️ -->

<!-- Don't forget that .self does not stop propagation -->
<div @click.self="onSelf">                                     <!-- ⚠️ -->

<!-- Don't use .exact when any modifier should work -->
<button @click.ctrl.exact="anyCtrl">                           <!-- ⚠️ -->

<!-- Don't rely on modifier order for unrelated behavior -->
<a @click.prevent.stop="doThis">                               <!-- ⚠️ -->

Common Pitfalls

PitfallWhy It HappensFix
.passive.prevent warningBrowser ignores preventDefaultRemove one modifier
Handler fires on child clicks.self not usedAdd .self
Event bubbles past element.stop not usedAdd .stop
Page reloads on submit.prevent not usedAdd .prevent
.once handler never fires againRegistered with { once: true }Re-register or use a flag
.capture order surprisingCapture fires before targetUnderstand phase order
.exact excludes intended keysToo strictRemove .exact
Modifier order affects resultLeft-to-right evaluationOrder deliberately

Real-World Examples

1. Form Submit

<form @submit.prevent="handleSubmit">

2. Click Outside to Close

<div @click.self="closeDropdown">

3. Modal Backdrop

<div @click.self="closeModal" class="backdrop">

4. Stop Link Navigation

<a @click.prevent="handleClick" href="#">Link</a>

5. Stop Event Bubbling in List

<li @click.stop="selectItem(item)">

6. Initialize Once

<button @click.once="initialize">

7. Scroll Performance

<div @scroll.passive="trackScroll">

8. Ctrl+Click for New Tab

<a @click.ctrl="openInNewTab" href="/path">

9. Keyboard Shortcut

<input @keyup.ctrl.enter="submit">

10. Right-Click Context Menu

<div @click.right.prevent="showContextMenu">

Visual

Propagation and Modifiers

┌─────────────────────────────────────────────────────────────┐
│  EVENT PROPAGATION WITH .stop                               │
│                                                             │
│  <div @click="outer">                                       │
│    <button @click.stop="inner">Click</button>               │
│  </div>                                                     │
│                                                             │
│  Click on button:                                           │
│    │                                                        │
│    ├── button handler fires (inner)                         │
│    │     │                                                  │
│    │     └── .stop prevents propagation                     │
│    │                                                        │
│    └── div handler does NOT fire                            │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  WITHOUT .stop                                              │
│                                                             │
│    ├── button handler fires (inner)                         │
│    │                                                        │
│    └── div handler fires (outer)  ← bubbles up              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Capture vs Bubbling Phase

┌─────────────────────────────────────────────────────────────┐
│  CAPTURE PHASE (Top → Down)                                 │
│                                                             │
│  document                                                   │
│    │  @click.capture                                        │
│    ▼                                                        │
│  <div @click.capture>                                       │
│    │                                                        │
│    ▼                                                        │
│  <button @click>   ← target                                 │
│                                                             │
│  Handler order: capture handlers fire first,                │
│  from outermost to innermost.                               │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  BUBBLING PHASE (Bottom → Up)                               │
│                                                             │
│  <button @click>   ← target                                 │
│    │                                                        │
│    ▼                                                        │
│  <div @click>                                               │
│    │                                                        │
│    ▼                                                        │
│  document                                                   │
│                                                             │
│  Handler order: bubble handlers fire after target,          │
│  from innermost to outermost.                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

.self Target Check

┌─────────────────────────────────────────────────────────────┐
│  .self: FIRES ONLY WHEN TARGET IS THE ELEMENT               │
│                                                             │
│  <div @click.self="onBackdrop">                             │
│    <div class="modal">Content</div>                         │
│  </div>                                                     │
│                                                             │
│  Click on backdrop:                                         │
│    target = <div @click.self>                               │
│    currentTarget = <div @click.self>                        │
│    target === currentTarget → handler fires                 │
│                                                             │
│  Click on modal content:                                    │
│    target = <div class="modal">                             │
│    currentTarget = <div @click.self>                        │
│    target !== currentTarget → handler skipped               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

.passive Performance

┌─────────────────────────────────────────────────────────────┐
│  WITHOUT .passive                                           │
│                                                             │
│  User scrolls                                               │
│    │                                                        │
│    ▼                                                        │
│  Browser waits for handler to complete                      │
│    │                                                        │
│    ▼                                                        │
│  Handler runs (may call preventDefault)                     │
│    │                                                        │
│    ▼                                                        │
│  Browser scrolls                                            │
│                                                             │
│  Result: janky scroll if handler is slow                    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  WITH .passive                                              │
│                                                             │
│  User scrolls                                               │
│    │                                                        │
│    ├── Browser scrolls immediately                          │
│    │                                                        │
│    └── Handler runs in parallel                             │
│                                                             │
│  Result: smooth scroll, no blocking                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
.stopCalls stopPropagation()
.preventCalls preventDefault()
.selfOnly fires if target is the element
.captureRegisters in capture phase
.onceFires at most once
.passiveSignals no preventDefault
.ctrlRequires Ctrl
.altRequires Alt
.shiftRequires Shift
.metaRequires Meta/Cmd
.exactExcludes other system modifiers
Modifier orderApplied left to right

Key takeaways:

  • Modifiers are declarative wrappers around DOM event behavior. .stop calls stopPropagation(), .prevent calls preventDefault(), and .capture, .once, and .passive set options on addEventListener. The template expresses the intent without the imperative calls.
  • Modifiers are applied in order. The chain is compiled into a single handler that runs each modifier in sequence before invoking your method. For .stop and .prevent, the order rarely matters, but the convention is .stop before .prevent.
  • .self is a runtime check, not a registration option. It fires the handler only when event.target === event.currentTarget. It does not stop propagation.
  • .passive and .prevent are incompatible. A passive listener cannot prevent the default action. Combining them produces a warning and no prevention.
  • .exact tightens system modifier matching. Without it, @click.ctrl fires when Ctrl is held and any other keys may be held. With it, the handler fires only when Ctrl is the only system modifier.
  • Key modifiers filter keyboard events. .enter, .esc, .tab, and the arrow keys are shorthands for checking event.key. They combine with system modifiers: @keyup.ctrl.enter fires when Ctrl and Enter are held together.
  • Capture-phase listeners fire before target-phase listeners. This is the opposite of the bubbling order. Use .capture when an ancestor needs to intercept events before descendants handle them.

Remember: Event modifiers are not a convenience layer over event.preventDefault(). They are the declarative interface to the DOM event model, and they express intent at the point where the event is bound. The .stop and .prevent modifiers replace two lines of imperative code with two words in the template. .self, .capture, .once, and .passive replace registration options that would otherwise require a mounted hook and manual addEventListener calls. Use them consistently, understand what each one does at the DOM level, and the event handling in your components will be shorter, clearer, and less prone to the propagation bugs that plague imperative listeners.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!