Handling Events

Listen to DOM events in a template with v-on, its @ shorthand, inline and method handlers, and the $event argument.

Syntaxv-on:click="handler" <!-- shorthand: @click="handler" -->

v-on is the directive that attaches a DOM event listener from inside a template. A directive is a special attribute Vue reads and acts on instead of passing through to HTML. This one takes the event name as its argument, so v-on:click listens for click.

Two ways to write the same thing

v-on:click="handler" and @click="handler" are identical. The @ form is shorthand for the v-on: form; the compiler produces the same listener for both. You will see both in other people's code, and mixing them in one file is legal if untidy.

Inline and method handlers

The value can be a JavaScript expression, called an inline handler: @click="count++". Or it can name a function, called a method handler: @click="greet". A method handler is passed the native DOM event as its first argument, so greet(event) receives a real MouseEvent.

Getting the event when you also pass arguments

Once you call the function yourself, the event is no longer passed for you. Use the special $event variable, @click="warn('nope', $event)", or wrap it in an arrow function, @click="(event) => warn('nope', event)". Both give you the same object.

One thing that catches people: a template unwraps refs, so @click="count++" works there. The same write in script is count.value++.

Example

Example · vue
<!-- Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const lastEvent = ref('none')

function add(amount: number): void {
  count.value += amount
}

function remember(label: string, event: MouseEvent): void {
  lastEvent.value = `${label}: ${event.type}`
}
</script>

<template>
  <p>Count: {{ count }}</p>

  <!-- inline handler: an expression, evaluated in this component -->
  <button type="button" @click="count++">Add 1</button>

  <!-- method handler, written with the v-on longhand -->
  <button type="button" v-on:click="add(5)">Add 5</button>

  <!-- $event is the native DOM event -->
  <button type="button" @click="remember('dollar', $event)">Remember</button>

  <!-- the same call, wrapped in an arrow function -->
  <button type="button" @click="(event) => remember('arrow', event)">
    Remember (arrow)
  </button>

  <p>Last event: {{ lastEvent }}</p>
</template>

<style scoped>
button {
  margin-right: 0.5rem;
}
</style>

When to use it

  • Increment a counter or toggle a panel from a button click.
  • Pass a row id to a delete function from inside a v-for.
  • Read event.target.value from an input before you know about v-model.
  • Record which key was released so a keyboard shortcut can respond.

More examples

Reading a value straight off the event

A value bound into the input, plus an input listener writing it back. This is exactly what v-model does for you, so it is worth reading once before you use the shorthand. The cast to HTMLInputElement is needed because event.target is typed as EventTarget, and vue-tsc checks template expressions too.

Example · vue
<!-- SearchBox.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const query = ref('')
</script>

<template>
  <input
    :value="query"
    @input="(event) => (query = (event.target as HTMLInputElement).value)"
  />
  <p>You typed: {{ query }}</p>
</template>

One handler, several buttons

Passing an argument from the template keeps one function for every button, instead of three near-identical ones. Reach for this whenever the only difference between handlers is a value. Note the :key on v-for, which the guide recommends whenever possible rather than strictly requiring, and that the loop variable is typed, so select(tab) type-checks.

Example · vue
<!-- TabBar.vue -->
<script setup lang="ts">
import { ref } from 'vue'

type Tab = 'profile' | 'billing' | 'keys'

const active = ref<Tab>('profile')
const tabs: Tab[] = ['profile', 'billing', 'keys']

function select(tab: Tab): void {
  active.value = tab
}
</script>

<template>
  <nav>
    <button
      v-for="tab in tabs"
      :key="tab"
      type="button"
      :aria-current="tab === active ? 'page' : undefined"
      @click="select(tab)"
    >
      {{ tab }}
    </button>
  </nav>
  <p>Showing: {{ active }}</p>
</template>

Several listeners in one object

v-on used without an event name accepts an object whose keys are event names. This has no @ shorthand, so it is always written v-on="...". It is useful when a set of listeners is built in script, for example when one object of handlers is applied to more than one element.

Example · vue
<!-- HotkeyField.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const status = ref('idle')

const handlers = {
  focus: () => (status.value = 'editing'),
  blur: () => (status.value = 'idle'),
  keyup: (event: KeyboardEvent) => (status.value = `key ${event.key}`)
}
</script>

<template>
  <!-- v-on with no argument takes an object of event name to handler -->
  <input v-on="handlers" />
  <p>{{ status }}</p>
</template>

Discussion

  • Be the first to comment on this lesson.