Skip to main content
Vue provides the v-on directive (shorthand @) to listen to DOM events and execute JavaScript when triggered.

Basic Event Handling

Listen to events using v-on or its shorthand @:
<template>
  <div>
    <!-- Full syntax -->
    <button v-on:click="count++">Increment</button>
    
    <!-- Shorthand -->
    <button @click="count++">Increment</button>
    
    <p>Count: {{ count }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

Method Handlers

For more complex logic, call a method:
<template>
  <div>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <p>Count: {{ count }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}

function decrement() {
  count.value--
}
</script>

Inline Handlers with Arguments

Pass arguments to event handlers:
<template>
  <div>
    <button @click="greet('Hello')">Say Hello</button>
    <button @click="greet('Goodbye')">Say Goodbye</button>
  </div>
</template>

<script setup>
function greet(message) {
  alert(message)
}
</script>

Accessing Event Object

Access the native DOM event:
<template>
  <!-- Method name - event passed automatically -->
  <button @click="handleClick">Click me</button>
</template>

<script setup>
function handleClick(event) {
  console.log(event.target)
  console.log(event.type) // 'click'
}
</script>

Event Modifiers

From packages/runtime-dom/src/directives/vOn.ts:30-47, Vue provides event modifiers to handle common event tasks:

Stop Propagation

<template>
  <!-- Stop event propagation -->
  <div @click="parentHandler">
    <button @click.stop="childHandler">Click me</button>
  </div>
</template>
The .stop modifier calls event.stopPropagation().

Prevent Default

<template>
  <!-- Prevent default behavior -->
  <form @submit.prevent="onSubmit">
    <button type="submit">Submit</button>
  </form>
  
  <!-- Modifier only -->
  <form @submit.prevent>
    <!-- Form won't submit -->
  </form>
</template>
The .prevent modifier calls event.preventDefault().

Modifier Chaining

<template>
  <!-- Modifiers can be chained -->
  <a @click.stop.prevent="doThat">Link</a>
</template>
Order matters when chaining modifiers. For example, @click.prevent.self will prevent click’s default action on the element itself and its children, while @click.self.prevent will only prevent click’s default action on the element itself.

Self Modifier

<template>
  <!-- Only trigger if event.target is the element itself -->
  <div @click.self="handleClick">
    <button>Won't trigger parent handler</button>
  </div>
</template>

Capture Mode

<template>
  <!-- Use capture mode when adding event listener -->
  <div @click.capture="doThis">
    <!-- Inner element clicks handled by outer handler first -->
  </div>
</template>

Once Modifier

<template>
  <!-- Event will be triggered at most once -->
  <button @click.once="handleClick">Click once</button>
</template>

Passive Modifier

<template>
  <!-- Improves scrolling performance on mobile -->
  <div @scroll.passive="onScroll">
    <!-- ... -->
  </div>
</template>
Don’t use .passive and .prevent together. .passive indicates to the browser that you won’t prevent the event’s default behavior, while .prevent does exactly that.

Available Event Modifiers

From packages/runtime-dom/src/directives/vOn.ts:30-47, the complete list of modifiers:
1

.stop

Calls event.stopPropagation() - stops event propagation
2

.prevent

Calls event.preventDefault() - prevents default behavior
3

.self

Only triggers if event.target is the element itself
4

.capture

Uses capture mode for event listener
5

.once

Triggers the event listener at most once
6

.passive

Adds listener as passive for better scroll performance

Key Modifiers

Listen for specific keys:
<template>
  <div>
    <!-- Only call when Enter is pressed -->
    <input @keyup.enter="submit" />
    
    <!-- Only call when PageDown is pressed -->
    <input @keyup.page-down="onPageDown" />
  </div>
</template>

Key Aliases

From packages/runtime-dom/src/directives/vOn.ts:75-86, Vue provides aliases for commonly used keys:
<template>
  <input @keyup.enter="submit" />
  <input @keyup.tab="handleTab" />
  <input @keyup.delete="handleDelete" />
  <input @keyup.esc="handleEscape" />
  <input @keyup.space="handleSpace" />
  <input @keyup.up="handleUp" />
  <input @keyup.down="handleDown" />
  <input @keyup.left="handleLeft" />
  <input @keyup.right="handleRight" />
</template>
The key mappings from source code:
const keyNames: Record<string, string> = {
  esc: 'escape',
  space: ' ',
  up: 'arrow-up',
  left: 'arrow-left',
  right: 'arrow-right',
  down: 'arrow-down',
  delete: 'backspace'
}

Any Valid Key

Use kebab-case for any key:
<template>
  <input @keyup.page-down="onPageDown" />
</template>

System Modifier Keys

From packages/runtime-dom/src/directives/vOn.ts:11, listen for modifier keys:
<template>
  <div>
    <!-- Ctrl + Click -->
    <button @click.ctrl="onClick">Ctrl+Click</button>
    
    <!-- Alt + Enter -->
    <input @keyup.alt.enter="clear" />
    
    <!-- Shift + Click -->
    <div @click.shift="handleShiftClick">Shift+Click</div>
    
    <!-- Cmd on Mac, Ctrl on Windows/Linux -->
    <button @click.meta="handleMetaClick">Cmd/Ctrl+Click</button>
  </div>
</template>

Exact Modifier

Require exact combination of modifiers:
<template>
  <div>
    <!-- Triggers even if Alt or Shift is also pressed -->
    <button @click.ctrl="onClick">Click</button>
    
    <!-- Only triggers when Ctrl and no other keys are pressed -->
    <button @click.ctrl.exact="onCtrlClick">Ctrl only</button>
    
    <!-- Only triggers when no system modifiers are pressed -->
    <button @click.exact="onClick">Click only</button>
  </div>
</template>

Mouse Button Modifiers

Restrict handler to specific mouse buttons:
<template>
  <div>
    <!-- Left click only -->
    <button @click.left="handleLeftClick">Left</button>
    
    <!-- Right click only -->
    <button @click.right="handleRightClick">Right</button>
    
    <!-- Middle click only -->
    <button @click.middle="handleMiddleClick">Middle</button>
  </div>
</template>
From the source at packages/runtime-dom/src/directives/vOn.ts:42-44:
const modifierGuards = {
  left: (e: Event) => 'button' in e && (e as MouseEvent).button !== 0,
  middle: (e: Event) => 'button' in e && (e as MouseEvent).button !== 1,
  right: (e: Event) => 'button' in e && (e as MouseEvent).button !== 2,
}

withModifiers Helper

Programmatically add modifiers from packages/runtime-dom/src/directives/vOn.ts:52-71:
import { withModifiers } from 'vue'

const handler = withModifiers(
  (event) => {
    // Handler code
  },
  ['stop', 'prevent']
)

withKeys Helper

Programmatically add key modifiers from packages/runtime-dom/src/directives/vOn.ts:91-162:
import { withKeys } from 'vue'

const handler = withKeys(
  (event) => {
    // Handler code
  },
  ['enter', 'space']
)

Multiple Event Handlers

Bind multiple handlers to the same event:
<template>
  <button @click="handler1(), handler2()">Click</button>
</template>

<script setup>
function handler1() {
  console.log('Handler 1')
}

function handler2() {
  console.log('Handler 2')
}
</script>

Dynamic Events

Bind events dynamically:
<template>
  <div>
    <!-- Dynamic event name -->
    <button @[eventName]="handleEvent">Click or Hover</button>
    
    <select v-model="eventName">
      <option value="click">Click</option>
      <option value="mouseenter">Hover</option>
    </select>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const eventName = ref('click')

function handleEvent() {
  console.log(`${eventName.value} event triggered`)
}
</script>

Common Event Types

<template>
  <div
    @click="onClick"
    @dblclick="onDoubleClick"
    @mousedown="onMouseDown"
    @mouseup="onMouseUp"
    @mousemove="onMouseMove"
    @mouseenter="onMouseEnter"
    @mouseleave="onMouseLeave"
    @contextmenu.prevent="onContextMenu"
  >
    Interact with me
  </div>
</template>

Event Handler Patterns

Debouncing

<template>
  <input @input="debouncedSearch" placeholder="Search..." />
</template>

<script setup>
import { ref } from 'vue'

const searchQuery = ref('')
let debounceTimer = null

function debouncedSearch(event) {
  clearTimeout(debounceTimer)
  debounceTimer = setTimeout(() => {
    searchQuery.value = event.target.value
    performSearch(searchQuery.value)
  }, 300)
}

function performSearch(query) {
  console.log('Searching for:', query)
}
</script>

Throttling

<script setup>
let lastCall = 0
const throttleDelay = 100

function throttledHandler(event) {
  const now = Date.now()
  if (now - lastCall < throttleDelay) return
  
  lastCall = now
  // Handle event
  console.log('Throttled handler called')
}
</script>

Conditional Event Handling

<template>
  <button @click="handleClick" :disabled="isLoading">
    {{ isLoading ? 'Loading...' : 'Submit' }}
  </button>
</template>

<script setup>
import { ref } from 'vue'

const isLoading = ref(false)

function handleClick() {
  if (isLoading.value) return
  
  isLoading.value = true
  // Perform async operation
  setTimeout(() => {
    isLoading.value = false
  }, 2000)
}
</script>

Best Practices

1

Use method handlers for complex logic

Keep template expressions simple by using methods for anything beyond basic operations.
2

Prevent default for form submissions

Always use .prevent on form submit events to avoid page reloads.
<form @submit.prevent="onSubmit">
3

Use appropriate modifiers

Take advantage of event modifiers to write cleaner, more declarative code.
4

Consider performance

For high-frequency events (scroll, mousemove), consider debouncing or throttling.

Template Syntax

Learn about directives and expressions

Forms

Handle form input with v-model

Reactivity Fundamentals

Understand reactive state updates

Build docs developers (and LLMs) love