Vue 3 v-on: handle events with @click, $event and modifiers
verified on 7 September 2026 · 6 min
v-on:click, or its @click shorthand, runs code when the event fires. Pass the name of a function to receive the native event, or call it with its arguments and $event if you need both. The .prevent, .stop, .once, .enter or .esc modifiers replace the repetitive event-handling code.
So far, the application displays things. It does not react to anything. The v-on directive attaches an event listener to an element and runs code when the event fires. This chapter covers everything you do with it day to day: a click, a hover, a key press, with or without an argument, and the modifiers that save you writing preventDefault() by hand.
The cart
Add a counter and a button:
<script setup>
import { ref } from 'vue'
const product = ref('T-Shirt')
const image = ref('/images/t-shirt-bleu.svg')
const inventory = ref(100)
const cart = ref(0)
const details = ref(['60 % coton', '30 % laine', '10 % polyester'])
const variants = ref([
{ id: 2234, color: 'Bleu', image: '/images/t-shirt-bleu.svg' },
{ id: 2235, color: 'Rouge', image: '/images/t-shirt-rouge.svg' }
])
</script>
<template>
<div class="product-display">
<div class="cart">Panier ({{ cart }})</div>
…
<button class="button">Ajouter au panier</button>
</div>
</template>Listening for a click
v-on: followed by the name of the event, with the code to run between quotes:
<button class="button" v-on:click="cart += 1">Ajouter au panier</button>Click: the counter goes up. The event name is the standard DOM one, with no prefix: click, submit, input, keyup, mouseover.
Calling a function
An inline expression is fine for a trivial operation. As soon as the logic runs past one line, write a function:
<script setup>
function addToCart() {
cart.value += 1
}
</script>
<template>
<button class="button" v-on:click="addToCart">Ajouter au panier</button>
</template>Note the .value in the script: we are handling the ref, not the unwrapped value. In the template, you always write {{ cart }} without .value.
@click="addToCart" passes the function itself: Vue calls it with the native event as its first argument. @click="addToCart()" calls it with no argument. Both work here. The classic mistake comes when the function expects a value: @click="updateImage(variant.image)" is correct, but leaving out the brackets would pass the event instead of the image path.
The @ shorthand
Just as v-bind has :, v-on has @:
<button class="button" @click="addToCart">Ajouter au panier</button>That is the form you will see throughout the documentation and in the rest of this tutorial.
Passing an argument
Let us make the image change when a colour is hovered. The function receives the image path of the hovered variant:
<script setup>
function updateImage(variantImage) {
image.value = variantImage
}
</script>
<template>
<div
v-for="variant in variants"
:key="variant.id"
@mouseover="updateImage(variant.image)"
>
{{ variant.color }}
</div>
</template>Hover over “Rouge”: the image turns red. Two mechanisms combine here, the loop from chapter 4 and the attribute binding from chapter 2.
Getting the native event
With no argument, the function receives the DOM event. With arguments, ask for it explicitly with $event:
<button @click="voir">Sans argument</button>
<button @click="voirAvec('bleu', $event)">Avec arguments</button>function voir(event) {
console.log(event.target)
}
function voirAvec(couleur, event) {
console.log(couleur, event.target)
}The object received is the browser’s native event, with no wrapper: event.target, event.key and event.preventDefault() work exactly as they do in plain JavaScript.
Event modifiers
Vue adds suffixes that spare you the repetitive event-handling code:
<form @submit.prevent="envoyer">…</form> <!-- event.preventDefault() -->
<div @click.stop="…">…</div> <!-- event.stopPropagation() -->
<div @click.self="…">…</div> <!-- seulement si la cible est cet élément -->
<button @click.once="…">…</button> <!-- une seule fois -->
<input @keyup.enter="valider"> <!-- seulement la touche Entrée -->The full list, with what each modifier replaces:
| Modifier | JavaScript equivalent | Typical use |
|---|---|---|
.prevent | event.preventDefault() | a form that must not reload the page |
.stop | event.stopPropagation() | a button inside a clickable card: the click does not bubble up to the card |
.self | if (event.target !== event.currentTarget) return | closing a modal when the backdrop is clicked, not its content |
.once | addEventListener(…, { once: true }) | an “Order” button that must not submit twice |
.capture | addEventListener(…, { capture: true }) | intercepting the event before the children |
.passive | addEventListener(…, { passive: true }) | scrolling and touch on mobile, so as not to block rendering |
Modifiers can be chained and read from left to right: @click.stop.prevent stops propagation, then cancels the default behaviour. The order matters with .self: @click.self.prevent cancels the default only for clicks on the element itself, @click.prevent.self cancels it for every click, children included.
@submit.prevent will be used in chapter 10 to stop the page reloading when the form is submitted.
Key and mouse modifiers
On keyboard events, a modifier filters the key. Vue accepts any key name exposed by KeyboardEvent.key, written in kebab-case, plus a few aliases:
<input @keyup.enter="valider">
<input @keyup.esc="annuler">
<input @keydown.tab="suivant">
<input @keyup.page-down="pageSuivante"> <!-- KeyboardEvent.key === 'PageDown' -->
<input @keydown.ctrl.enter="envoyer"> <!-- Ctrl + Entrée -->The available aliases: .enter, .tab, .delete (both Delete and Backspace), .esc, .space, .up, .down, .left, .right. The system keys .ctrl, .alt, .shift and .meta combine with the others. And .exact requires that no other system key be held down:
<button @click.ctrl="ouvrir">…</button> <!-- Ctrl + clic, même si Shift est aussi enfoncé -->
<button @click.ctrl.exact="ouvrir">…</button> <!-- Ctrl + clic, et rien d'autre -->
<button @click.exact="ouvrir">…</button> <!-- clic sans aucune touche système -->For the mouse, .left, .right and .middle filter the button: @click.right.prevent="menuContextuel" replaces the browser’s menu with your own.
Several handlers on the same event
One event can trigger several functions, separated by commas. Each function is then called with its brackets:
<button @click="addToCart(), trackClick('ajout-panier')">Ajouter au panier</button>Beyond two calls it stops being readable: write one function that calls the others.
Recap
v-on:clickor@click: an inline expression for a trivial operation, otherwise a function.- Without brackets, the function receives the native event; with arguments,
$eventpasses it along as well. .prevent,.stop,.selfand.oncereplace the repetitive code; key modifiers filter the keyboard;.exactlocks down the combinations.- In the script, the state is changed through
.value; in the template, without it.
The functions live in the methods option and reach the state through this: methods: { addToCart() { this.cart += 1 } }. The template itself does not change.
The events in this chapter come from the DOM. For a child component to notify its parent, Vue offers custom events with emit: that is the subject of chapter 9, covered in detail in the complete guide to Vue emit.
Common errors
cart += 1 does nothing useful on a ref: write cart.value += 1.@click="updateImage" passes the native event instead of the expected value. With an argument, call the function: @click="updateImage(variant.image)".$event: @click="faire('bleu', $event)".@click.prevent.self prevents the default behaviour of every click, @click.self.prevent only of those whose target is the element itself. The order reads from left to right..passive promises the browser that preventDefault() will not be called; combined with .prevent, the browser ignores one of the two and prints a warning.