Event Modifiers

Dot suffixes on v-on that replace preventDefault, stopPropagation, once, capture and key checks inside your handler.

Syntax@click.stop.prevent="handler"

An event modifier is a dot suffix on v-on that handles the event-plumbing part of a listener, so your function is left with the logic. @submit.prevent="save" keeps the page from reloading without save touching the event at all.

What each one replaces

ModifierPlain JavaScript
.preventevent.preventDefault()
.stopevent.stopPropagation()
.selfif (event.target !== event.currentTarget) return
.onceaddEventListener(type, fn, { once: true })
.captureaddEventListener(type, fn, { capture: true })
.passiveaddEventListener(type, fn, { passive: true })

Keys, modifier keys and buttons

Key modifiers filter by key: .enter, .tab, .delete (which covers Delete and Backspace), .esc, .space, .up, .down, .left, .right. Any other key works in kebab-case, so @keyup.page-down listens for PageDown. The system modifiers .ctrl, .alt, .shift and .meta require that key to be held, and .exact requires that no others are. .left, .right and .middle restrict mouse events to one button.

Order matters

Modifiers apply in the order written. @click.prevent.self prevents the default for the element and its children; @click.self.prevent prevents it only when the element itself was clicked. They can be chained freely, and a modifier can stand alone: <form @submit.prevent> is a valid listener that only blocks the reload.

Example

Example · vue
<!-- InvoiceList.vue -->
<script setup lang="ts">
import { ref } from 'vue'

type Row = { id: number; label: string }

const rows = ref<Row[]>([
  { id: 1, label: 'Invoice 1041' },
  { id: 2, label: 'Invoice 1042' }
])
const openId = ref<number | null>(null)
const message = ref('')

function remove(id: number): void {
  rows.value = rows.value.filter((row) => row.id !== id)
}
</script>

<template>
  <!-- .prevent: the form does not reload the page -->
  <form @submit.prevent="message = 'searched'">
    <!-- .enter and .esc fire only for those keys -->
    <input @keyup.enter="message = 'enter pressed'" @keyup.esc="message = ''" />
  </form>

  <ul>
    <li v-for="row in rows" :key="row.id" @click="openId = row.id">
      {{ row.label }}
      <!-- .stop: deleting does not also open the row -->
      <button type="button" @click.stop="remove(row.id)">Delete</button>
    </li>
  </ul>

  <!-- .once: the listener is removed after the first call -->
  <button type="button" @click.once="message = 'dismissed'">Dismiss</button>

  <p>Open: {{ openId }} — {{ message }}</p>
</template>

<style scoped>
li {
  cursor: pointer;
}
</style>

When to use it

  • Stop a form submit from reloading the page.
  • Keep a row's delete button from also opening the row.
  • Submit a single-line field when Enter is released.
  • Run a one-off dismiss handler without removing the listener yourself.

More examples

The modifier and the longhand side by side

These two forms behave the same. Seeing them together makes the modifier readable in someone else's template and tells you what to write when you need a condition the modifiers cannot express, such as preventing the default only for invalid input.

Example · vue
<!-- SaveForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const saved = ref(0)

// with .prevent, the handler never sees the event
function save(): void {
  saved.value += 1
}

// without it, the handler does the plumbing itself
function saveManually(event: Event): void {
  event.preventDefault()
  saved.value += 1
}
</script>

<template>
  <form @submit.prevent="save">
    <button type="submit">Save (modifier)</button>
  </form>

  <form @submit="saveManually">
    <button type="submit">Save (by hand)</button>
  </form>

  <p>Saved {{ saved }} times</p>
</template>

Modifier keys, .exact and a mouse button

Use these when a shortcut has to be distinguished from a plain interaction. Without .exact, @click.ctrl also fires for Ctrl+Shift+click, which is how a shortcut ends up firing twice. A system modifier must be held at the moment the event fires, so @keyup.ctrl does not fire when you release Ctrl itself.

Example · vue
<!-- CanvasTools.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const action = ref('none')
</script>

<template>
  <!-- fires even if Shift or Alt is also held -->
  <button type="button" @click.ctrl="action = 'ctrl click'">Ctrl</button>

  <!-- fires only when Ctrl alone is held -->
  <button type="button" @click.ctrl.exact="action = 'ctrl only'">Ctrl exact</button>

  <!-- fires only when no system modifier is held -->
  <button type="button" @click.exact="action = 'plain click'">Plain</button>

  <!-- Alt + Enter inside a field -->
  <input @keyup.alt.enter="action = 'alt enter'" />

  <!-- secondary mouse button, with the browser menu suppressed -->
  <div @mousedown.right.prevent="action = 'custom menu'">Right-click me</div>

  <p>{{ action }}</p>
</template>

A backdrop that closes only on its own clicks

The classic click-outside-to-close pattern. .self compares event.target with the element the listener sits on, so clicks inside the panel do not close the dialog and you do not need .stop on every child. Keyboard dismissal and focus handling come later, in the accessibility stage.

Example · vue
<!-- ConfirmDialog.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <button type="button" @click="open = true">Delete account</button>

  <!-- .self: a click that bubbled up from the panel is ignored -->
  <div v-if="open" class-less @click.self="open = false">
    <div>
      <p>This cannot be undone.</p>
      <button type="button" @click="open = false">Cancel</button>
    </div>
  </div>
</template>

<style scoped>
div > div {
  border: 1px solid currentColor;
  padding: 1rem;
}
</style>

Discussion

  • Be the first to comment on this lesson.