Chapter 5 of 11

Vue 3 v-on: handle events with @click, $event and modifiers

verified on 7 September 2026 · 6 min

Quick answer

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.

By the end of this chapter, you will know how to react to clicks, hovers and key presses, pass arguments to your handlers, get hold of the native event and use modifiers to write less 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:

src/App.vue
<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:

src/App.vue
<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:

src/App.vue
<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.

With or without brackets?

@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 @:

src/App.vue
<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:

src/App.vue
<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:

html
<button @click="voir">Sans argument</button>
<button @click="voirAvec('bleu', $event)">Avec arguments</button>
javascript
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:

html
<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:

html
<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:

html
<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:

html
<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:click or @click: an inline expression for a trivial operation, otherwise a function.
  • Without brackets, the function receives the native event; with arguments, $event passes it along as well.
  • .prevent, .stop, .self and .once replace the repetitive code; key modifiers filter the keyboard; .exact locks down the combinations.
  • In the script, the state is changed through .value; in the template, without it.
In the Options API

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.

ExerciseAdd a “Remove from cart” button that decrements the counter without ever going below zero. Then replace the hover on the colours with a click, and compare how the two feel.

Common errors

Forgetting .value inside a function In the script, cart += 1 does nothing useful on a ref: write cart.value += 1.
Missing brackets with an argument @click="updateImage" passes the native event instead of the expected value. With an argument, call the function: @click="updateImage(variant.image)".
Getting the event when you already pass an argument You have to ask for it explicitly with $event: @click="faire('bleu', $event)".
Chaining modifiers in the wrong order @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.
Using .passive with .prevent .passive promises the browser that preventDefault() will not be called; combined with .prevent, the browser ignores one of the two and prints a warning.
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.