Chapter 80: Vue v-on Directive
V-on (99% of the time written with its beautiful shorthand: @)
This directive is Vue’s official way to listen to DOM events (click, submit, keyup, mouseenter, change…) and custom events emitted from child components.
In plain words:
“Hey Vue, whenever this element (or child component) fires this event, please run my function.”
Without v-on / @, you would have to manually do addEventListener in onMounted and removeEventListener in onBeforeUnmount — ugly, error-prone, and not reactive.
With @click, @submit, @keyup.enter… Vue handles everything for you: attaching, detaching, passing the event object, supporting modifiers, and more.
1. Basic Syntax – The Shorthand Everyone Uses
|
0 1 2 3 4 5 6 7 8 |
<!-- These two lines are 100% identical --> <button v-on:click="increment">Click me</button> <button @click="increment">Click me</button> |
Shorthand rule (burn this into your brain)
@ = v-on:
So everywhere you see @click, @submit, @keydown.enter — it’s just sugar for v-on:click, v-on:submit, v-on:keydown.enter
2. Core Features & Modifiers (What Makes @ So Powerful)
Vue gives you event modifiers that save you from writing a lot of boilerplate JavaScript.
| Modifier | What it does | Typical usage example | Equivalent vanilla JS |
|---|---|---|---|
| .stop | event.stopPropagation() | @click.stop — stop bubbling to parent | e.stopPropagation() |
| .prevent | event.preventDefault() | @submit.prevent — prevent form submit reload | e.preventDefault() |
| .capture | Add listener in capture phase | @click.capture | { capture: true } |
| .self | Only trigger if event.target === element | @click.self — ignore child clicks | if (e.target !== el) |
| .once | Listener removed after first trigger | @click.once — track one-time analytics | el.addEventListener(…, { once: true }) |
| .passive | Improves scroll/touch performance | @touchmove.passive — large scroll areas | { passive: true } |
Key combinations (very common)
|
0 1 2 3 4 5 6 7 8 9 10 |
<input @keyup.enter="submitForm" /> <input @keyup.enter.prevent="submitForm" /> <!-- most common for forms --> <div @click.self="handleContainerClick"> <!-- ignore child clicks --> <button>Click me – won't bubble</button> </div> |
3. Real, Complete Example – Form with Multiple Event Types
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
<template> <form @submit.prevent="handleSubmit" class="contact-form"> <div class="field"> <label>Name</label> <input v-model.trim="form.name" @focus="onFocus('name')" @blur="onBlur('name')" @keyup.enter="submitIfValid" placeholder="Your name" required /> </div> <div class="field"> <label>Email</label> <input v-model.trim="form.email" type="email" @input="validateEmail" placeholder="you@company.com" required /> <small v-if="emailError" class="error">{{ emailError }}</small> </div> <div class="field"> <label>Message</label> <textarea v-model.trim="form.message" @keydown.ctrl.enter="submitIfValid" placeholder="Your message..." rows="5" required ></textarea> </div> <!-- Multiple event listeners on same element --> <button type="submit" :disabled="!formIsValid || isSubmitting" @click.left="trackLeftClick" @click.right.prevent="showContextMenu" @mouseenter="onHover" @mouseleave="onLeave" > {{ isSubmitting ? 'Sending…' : 'Send Message' }} </button> <p v-if="submitSuccess" class="success">Message sent successfully!</p> <p v-if="submitError" class="error">{{ submitError }}</p> </form> </template> <script setup lang="ts"> import { reactive, ref, computed } from 'vue' const form = reactive({ name: '', email: '', message: '' }) const isSubmitting = ref(false) const submitSuccess = ref(false) const submitError = ref<string | null>(null) const emailError = ref<string | null>(null) const formIsValid = computed(() => { return ( form.name.trim().length >= 2 && form.email.includes('@') && form.message.trim().length >= 10 ) }) function validateEmail() { if (form.email && !form.email.includes('@')) { emailError.value = 'Please enter a valid email' } else { emailError.value = null } } function onFocus(field: string) { console.log(`${field} field focused`) } function onBlur(field: string) { console.log(`${field} field blurred`) } function onHover() { console.log('Button hovered') } function onLeave() { console.log('Button left') } function trackLeftClick() { console.log('Left click detected') } function showContextMenu() { alert('Right-click context menu (prevented default)') } async function handleSubmit() { if (!formIsValid.value) return isSubmitting.value = true submitError.value = null submitSuccess.value = false try { // Fake API call await new Promise(r => setTimeout(r, 1200)) submitSuccess.value = true form.name = '' form.email = '' form.message = '' } catch (err: any) { submitError.value = err.message || 'Failed to send message' } finally { isSubmitting.value = false } } function submitIfValid() { if (formIsValid.value) { handleSubmit() } } </script> <style scoped> .contact-form { max-width: 500px; margin: 3rem auto; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .field { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 500; } input, textarea { width: 100%; padding: 0.9rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 1rem; } textarea { resize: vertical; } button { width: 100%; padding: 0.9rem; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; margin-top: 1rem; } button:disabled { opacity: 0.6; cursor: not-allowed; } .error { color: #dc2626; margin-top: 0.5rem; font-size: 0.9rem; } .success { color: #16a34a; margin-top: 1rem; text-align: center; font-weight: 500; } </style> |
Quick Summary Table – v-on / @ Modifiers & Common Patterns
| Modifier / Syntax | What it does | Typical real-world usage |
|---|---|---|
| @click | Listen to click event | Buttons, cards |
| @submit.prevent | Prevent form reload | Almost every <form> |
| @keyup.enter | Only on Enter key | Submit form on input Enter |
| @click.stop | Stop event bubbling | Click inside modal shouldn’t close it |
| @click.once | Run only once | Analytics tracking, onboarding tooltips |
| @click.self | Only if clicked directly on element (not children) | Container clicks ignoring child buttons |
| @click.right.prevent | Right-click + prevent context menu | Custom context menus |
| @keydown.ctrl.enter | Ctrl + Enter | Submit large textareas |
| @click.left | Only left-click (not middle/right) | Distinguish mouse buttons |
Pro Tips from Real Projects (Hyderabad 2026)
- Always use .prevent on @submit — prevents page reload
- Use .stop when you have nested clickable elements (modal overlay vs modal content)
- Use .once for one-time actions (track first click, onboarding)
- Use .self to ignore child clicks (click on modal backdrop to close)
- Use .passive on scroll/touch events for better performance
- Combine modifiers: @click.stop.prevent.self — very common in modals
- For custom events from child components → @custom-event=”handler”
Your mini practice task:
- Build the form above
- Add validation messages with v-if
- Use @keyup.enter.prevent on inputs to submit
- Add @click.stop on modal content (once you add modal)
- Add @click.once on a button to track first click
Any part confusing? Want full examples for:
- Modal with @click.self on overlay + @click.stop on content?
- Form with @submit.prevent + @keydown.ctrl.enter?
- Custom component emitting events + parent listening with @?
- All common modifiers in one realistic dashboard card?
Just tell me — we’ll build the next event-rich component together 🚀
