Vue Event Handling
Listening to Events — v-on / @
Vue uses v-on (shorthand @) to listen to DOM events and run JavaScript when they fire. You can pass a method reference, an inline expression, or an arrow function.
<template>
<div>
<!-- Method reference -->
<button @click="handleClick">Click me</button>
<!-- Inline expression -->
<button @click="count++">Count: {{ count }}</button>
<!-- Arrow function — access event object -->
<button @click="(e) => handleWithEvent(e, 'hello')">With args</button>
<!-- $event — pass event in inline handler -->
<input @input="handleInput($event)">
<!-- Multiple events -->
<input
@focus="isFocused = true"
@blur="isFocused = false"
@keyup.enter="submit"
>
<!-- Mouse events -->
<div
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
:class="{ hovered: isHovered }"
>
Hover me
</div>
<p>Count: {{ count }}, Focused: {{ isFocused }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const isFocused = ref(false)
const isHovered = ref(false)
function handleClick() {
alert('Button clicked!')
}
function handleWithEvent(event, message) {
console.log(message, event.target)
}
function handleInput(event) {
console.log('Input value:', event.target.value)
}
function submit() {
console.log('Form submitted via Enter key')
}
</script>
<template>
<div>
<!-- Event modifiers -->
<!-- .prevent — calls event.preventDefault() -->
<form @submit.prevent="handleSubmit">
<button type="submit">Submit (no page reload)</button>
</form>
<!-- .stop — calls event.stopPropagation() -->
<div @click="outerClick">
Outer
<button @click.stop="innerClick">Inner (stops bubbling)</button>
</div>
<!-- .once — fires only once -->
<button @click.once="fireOnce">Click once only</button>
<!-- .self — only fires if target is the element itself -->
<div @click.self="selfOnly" class="box">
Click the box (not children)
<span>I'm a child</span>
</div>
<!-- .passive — improves scroll performance -->
<div @scroll.passive="handleScroll">...</div>
<!-- Key modifiers -->
<input @keyup.enter="onEnter" placeholder="Press Enter" />
<input @keyup.esc="onEscape" placeholder="Press Escape" />
<input @keyup.space="onSpace" placeholder="Press Space" />
<input @keydown.ctrl.s.prevent="save" placeholder="Ctrl+S to save" />
<input @keydown.shift.enter="newLine" placeholder="Shift+Enter" />
<!-- Mouse button modifiers -->
<button @click.left="leftClick">Left click</button>
<button @click.right.prevent="rightClick">Right click</button>
<button @click.middle="middleClick">Middle click</button>
<!-- Chaining modifiers -->
<a href="#" @click.prevent.stop="handleLink">Link</a>
</div>
</template>
<script setup>
function handleSubmit() { console.log('Submitted!') }
function outerClick() { console.log('Outer clicked') }
function innerClick() { console.log('Inner clicked') }
function fireOnce() { console.log('Fired once!') }
function selfOnly() { console.log('Self clicked') }
function handleScroll() { /* passive scroll handler */ }
function onEnter() { console.log('Enter pressed') }
function onEscape() { console.log('Escape pressed') }
function onSpace() { console.log('Space pressed') }
function save() { console.log('Ctrl+S — Save!') }
function newLine() { console.log('Shift+Enter') }
function leftClick() { console.log('Left click') }
function rightClick() { console.log('Right click') }
function middleClick() { console.log('Middle click') }
function handleLink() { console.log('Link clicked') }
</script>
<!-- Child component: MyButton.vue -->
<template>
<button @click="handleClick" :disabled="loading">
<slot>{{ label }}</slot>
</button>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
label: { type: String, default: 'Click' }
})
// Declare emitted events
const emit = defineEmits({
// With validation
click: (payload) => {
return typeof payload === 'object'
},
// Simple declaration
'update:loading': Boolean,
success: null,
error: String,
})
const loading = ref(false)
async function handleClick() {
loading.value = true
emit('update:loading', true)
try {
// Simulate async work
await new Promise(r => setTimeout(r, 1000))
emit('click', { timestamp: Date.now(), source: 'button' })
emit('success')
} catch (err) {
emit('error', err.message)
} finally {
loading.value = false
emit('update:loading', false)
}
}
</script>
<!-- Parent component -->
<template>
<div>
<MyButton
label="Save"
@click="onButtonClick"
@success="onSuccess"
@error="onError"
/>
<p>{{ status }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import MyButton from './MyButton.vue'
const status = ref('Ready')
function onButtonClick(payload) {
console.log('Clicked at:', payload.timestamp)
status.value = 'Processing...'
}
function onSuccess() { status.value = 'Saved!' }
function onError(msg) { status.value = `Error: ${msg}` }
</script>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.