Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 077 / 100

Event handling in Vue: a Vue & Nuxt guide for developers

Respond to user actions.

Expona Admin6 min read

Event handling in Vue: a Vue & Nuxt guide for developers

User interactions form the backbone of any interactive web application, and handling those interactions efficiently is where Vue shines. Whether you're building a simple form submission or a complex multi-step checkout flow, understanding event handling in Vue is essential for building responsive, maintainable applications.

Why event handling matters in production

In our experience across e-commerce and SaaS platforms, poorly implemented event handling leads to hard-to-debug issues: double submissions, race conditions, and confusing user flows. Vue's declarative event system lets us express intent clearly in templates, making code reviews easier and reducing bugs before they reach production. By mastering event handling, we prevent common pitfalls and build interfaces that feel snappy and predictable.

@click and other v-on shortcuts

The most fundamental event handler in Vue is @click, which is syntactic sugar for v-on:click. In practice, we use the shorthand almost exclusively because it keeps templates readable.

typescript
<template>
  <button @click="handleClick">Click me</button>
  <button @click="count++">Increment</button>
</template>

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

const count = ref(0)

function handleClick() {
  console.log('Button clicked')
}
</script>

Beyond click events, we commonly handle form and input events. The @input event fires on every keystroke, while @change fires only when the user finishes editing and blurs the field. In real-world forms, we typically use @input for immediate validation feedback and @change for heavier operations.

typescript
<template>
  <input 
    @input="query = $event.target.value" 
    placeholder="Search products"
  >
  <input 
    @change="saveUserPreference" 
    type="checkbox"
  >
</template>

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

const query = ref('')

function saveUserPreference() {
  // Only called when user finishes interacting
}
</script>

Other common event handlers include @submit for forms, @focus and @blur for input fields, and @mouseover for hover interactions. Each serves a specific purpose in the user interaction lifecycle.

Modifiers like .prevent and .stop

Event modifiers are where Vue's event handling truly shines. Instead of calling event.preventDefault() or event.stopPropagation() manually, we declare intent directly in the template with modifiers.

.prevent prevents the default browser behavior. This is essential for form submissions in single-page applications where we handle the request ourselves:

typescript
<template>
  <form @submit.prevent="submitOrder">
    <input v-model="email" type="email" required>
    <button type="submit">Place Order</button>
  </form>
</template>

<script setup>
const email = ref('')

async function submitOrder() {
  // Handle submission without page reload
  await api.post('/orders', { email: email.value })
}
</script>

.stop prevents event bubbling. This matters when you have nested clickable elements and need to prevent parent handlers from firing:

typescript
<template>
  <div @click="selectRow(row)" class="row">
    <p>Row content</p>
    <button @click.stop="deleteRow(row)">Delete</button>
  </div>
</template>

<script setup>
function selectRow(row) {
  // Only fires when clicking the row itself, not the delete button
}

function deleteRow(row) {
  // Handles deletion without triggering selectRow
}
</script>

Other useful modifiers include .self (only trigger if the event target is the element itself), .once (remove the listener after first execution), and .capture (use capture phase instead of bubbling). You can chain multiple modifiers: @click.prevent.stop combines them.

Key modifiers like @keyup.enter

When handling keyboard input, key modifiers let us react to specific keys. Instead of checking event.key === 'Enter' in the handler, we declare it in the template:

typescript
<template>
  <input 
    v-model="searchQuery"
    @keyup.enter="search"
    placeholder="Press Enter to search"
  >
</template>

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

const searchQuery = ref('')

function search() {
  // Called only when user presses Enter
  console.log('Searching for:', searchQuery.value)
}
</script>

Vue provides named key modifiers for common keys: .enter, .tab, .escape, .space, .up, .down, .left, .right. For other keys, use their lowercase names directly, like @keyup.a or @keydown.delete.

System modifier keys are particularly useful: .ctrl, .shift, .alt, .meta (Command on Mac, Windows key on Windows). We use these frequently for keyboard shortcuts:

typescript
<template>
  <input 
    @keydown.ctrl.s="saveDocument"
    @keydown.shift.enter="submitForm"
  >
</template>

<script setup>
function saveDocument() {
  // Ctrl+S on Windows/Linux, Cmd+S on Mac
}

function submitForm() {
  // Only fires with Shift+Enter combination
}
</script>

In our experience building productivity tools, keyboard shortcuts dramatically improve user experience for power users. The combination of key and system modifiers covers most use cases without needing custom logic.

Emit custom events from components

Beyond built-in DOM events, we frequently need components to communicate upward to their parents. Vue's emit function lets child components send custom events with data.

In a typical e-commerce scenario, a product card component might emit an event when the user clicks "Add to cart":

typescript
<!-- ProductCard.vue -->
<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>${{ product.price }}</p>
    <button @click="addToCart">Add to Cart</button>
  </div>
</template>

<script setup>
defineProps({
  product: Object
})

const emit = defineEmits(['add-to-cart'])

function addToCart() {
  emit('add-to-cart', {
    productId: product.id,
    quantity: 1
  })
}
</script>

The parent component listens for this custom event using the same syntax as DOM events:

typescript
<!-- ProductList.vue -->
<template>
  <ProductCard 
    v-for="product in products"
    :key="product.id"
    :product="product"
    @add-to-cart="handleAddToCart"
  />
</template>

<script setup>
function handleAddToCart(item) {
  console.log('Adding to cart:', item)
  // Update cart state, call API, etc.
}
</script>

Defining events with defineEmits provides TypeScript support and makes the component's interface explicit. This is especially valuable in larger codebases where developers reviewing your code need to understand what events your component emits.

For complex component hierarchies, consider using provide/inject or a state management solution like Pinia instead of prop drilling and event emission chains. The choice depends on how deeply nested components are and whether the data flows one direction or needs to be shared across many branches.

Common mistakes

Forgetting $event in inline handlers: When you need to access the event object in an inline handler, always pass $event explicitly. @click="alert($event.target)" works; @click="alert(event.target)" does not.

Using .prevent without understanding when to apply it: We've seen teams blindly add .prevent to every form submission. Sometimes the default behavior is what you want—for example, when navigating to a new page. Use .prevent only when handling the event yourself.

Emit naming inconsistencies: Vue documentation recommends kebab-case for emitted events (add-to-cart) even though props use camelCase. Stick to this convention across your codebase to avoid confusion.

Not debouncing or throttling high-frequency events: Events like @mousemove or @scroll fire extremely frequently. Without debouncing or throttling, you'll trigger handlers hundreds of times per second, tanking performance. Use libraries like Lodash or implement simple debounce wrappers.

typescript
import { debounce } from 'lodash-es'

const handleScroll = debounce(() => {
  // This runs at most once every 300ms
}, 300)

// In template: @scroll="handleScroll"

Key takeaways

  • Use @click and other event shorthand syntax for cleaner, more readable templates
  • Leverage modifiers like .prevent, .stop, and key modifiers to express intent directly in templates rather than hiding logic in handler functions
  • Combine system modifiers (.ctrl, .shift) with key modifiers for keyboard shortcuts that improve user experience
  • Use defineEmits to define custom events in child components and let parents listen with the familiar @event-name syntax
  • Avoid common pitfalls: remember $event in inline handlers, debounce high-frequency events, and be consistent with event naming conventions
  • For deep component hierarchies with lots of event emission, consider Pinia or provide/inject as cleaner alternatives to prop drilling
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles