Home
Blogs
Series
Components
Temporary cover image for the Vue v-on article

Vue Directives: v-on

Learn how to use v-on in Vue with DOM events, modifiers, keyboard shortcuts, and best practices to keep components clear and maintainable.

Vue Directives: v-on

v-on connects DOM or component events to functions in your app. When the user clicks, types, or presses a key, v-on triggers the logic you defined.

Why it matters

Without v-on, an interface cannot respond to user interaction. With v-on, you can:

  • Capture user actions without manually manipulating the DOM.
  • Keep interactions declarative and readable in the template.
  • Separate state, rendering, and behavior in a way that stays consistent with Vue.

v-on is the foundation for buttons, forms, keyboard shortcuts, and event-based communication between components.

Core concept

Long form:

App.vue
<button v-on:click="increment">Increase</button>

Recommended shorthand:

App.vue
<button @click="increment">Increase</button>

v-on supports:

  • Native events (click, input, submit, keydown).
  • Component-emitted events (@save, @close).
  • Event modifiers (.prevent, .stop, .once, .self).
  • Keyboard modifiers (.enter, .esc, combinations with ctrl, shift, etc.).

When to use it

Use v-on whenever you need to react to user actions or component events.

Typical use cases:

  • Action buttons (@click="createTask").
  • Forms (@submit.prevent="submitForm").
  • Real-time input handling (@input="handleSearch").
  • Keyboard shortcuts (@keydown.ctrl.enter="publish").
  • Custom events emitted by child components (@save="persistTask").

When to avoid it

Avoid v-on in these cases:

  • When there is no real interaction and content is fully static.
  • When you are placing too much inline logic in the template; move it to functions or computed.
  • When you are trying to "solve" security with frontend events: real validation must happen in the backend.

Also avoid chaining too many modifiers without clear intent, since that hurts maintainability.

Common mistakes

1) Executing the function instead of referencing it

Incorrect:

Avoid
<button @click="saveTask()">Save</button>

This is valid, but if you do not need arguments, this is usually cleaner:

Recommended
<button @click="saveTask">Save</button>

2) Forgetting .prevent in forms

Incorrect:

Avoid
<form @submit="submitForm">

Correct:

Recommended
<form @submit.prevent="submitForm">

Without .prevent, the browser reloads the page by default.

3) Putting too much logic inside the template

Avoid:

Avoid
<button @click="isAdmin && canEdit && !isLocked ? publishNow() : showWarning()">
  Publish
</button>

Better:

Recommended
<button @click="handlePublishClick">Publish</button>

Then move the decision into a clear function in the script.

4) Using event without declaring it

Incorrect:

Avoid
<input @input="onInput(event)" />

Correct:

Recommended
<input @input="onInput($event)" />

Or better, type the event and read target safely with TypeScript.

Practical examples

1) Simple click to update state

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);
</script>
 
<template>
  <button @click="count++">Clicks: {{ count }}</button>
</template>
<script lang="ts">
export default {
  data() {
    return {
      count: 0,
    };
  },
};
</script>
 
<template>
  <button @click="count++">Clicks: {{ count }}</button>
</template>

2) Form submit with .prevent

<script setup lang="ts">
import { ref } from "vue";

const email = ref("");

function submitForm() {
  if (!email.value.trim()) return;
  console.log("Send:", email.value);
}
</script>
 
<template>
  <form @submit.prevent="submitForm">
    <input v-model="email" type="email" placeholder="you@email.com" />
    <button type="submit">Send</button>
  </form>
</template>
<script lang="ts">
export default {
  data() {
    return {
      email: "",
    };
  },
  methods: {
    submitForm() {
      if (!this.email.trim()) return;
      console.log("Send:", this.email);
    },
  },
};
</script>
 
<template>
  <form @submit.prevent="submitForm">
    <input v-model="email" type="email" placeholder="you@email.com" />
    <button type="submit">Send</button>
  </form>
</template>

3) Keyboard shortcut with modifiers

<script setup lang="ts">
import { ref } from "vue";

const note = ref("");

function saveDraft() {
  console.log("Draft saved:", note.value);
}
</script>
 
<template>
  <textarea v-model="note" @keydown.ctrl.enter.prevent="saveDraft" />
</template>
<script lang="ts">
export default {
  data() {
    return {
      note: "",
    };
  },
  methods: {
    saveDraft() {
      console.log("Draft saved:", this.note);
    },
  },
};
</script>
 
<template>
  <textarea v-model="note" @keydown.ctrl.enter.prevent="saveDraft" />
</template>

4) Custom event from a child component

<script setup lang="ts">
import TaskForm from "./TaskForm.vue";

function handleSave(taskTitle: string) {
  console.log("New task:", taskTitle);
}
</script>
 
<template>
  <TaskForm @save="handleSave" />
</template>
<script lang="ts">
import TaskForm from "./TaskForm.vue";

export default {
  components: { TaskForm },
  methods: {
    handleSave(taskTitle) {
      console.log("New task:", taskTitle);
    },
  },
};
</script>
 
<template>
  <TaskForm @save="handleSave" />
</template>

Full example

Task list component with:

  • @submit.prevent to create tasks.
  • @click to mark tasks as completed.
  • @keydown.enter for quick submit.
<script setup lang="ts">
import { computed, ref } from "vue";

type Task = {
  id: number;
  title: string;
  done: boolean;
};

const draft = ref("");
const tasks = ref<Task[]>([]);

const remaining = computed(() => tasks.value.filter((task) => !task.done).length);

function addTask() {
  const title = draft.value.trim();
  if (!title) return;

  tasks.value.push({
    id: Date.now(),
    title,
    done: false,
  });
  draft.value = "";
}

function toggleTask(id: number) {
  const task = tasks.value.find((item) => item.id === id);
  if (!task) return;
  task.done = !task.done;
}
</script>
 
<template>
  <section>
    <h2>Tasks</h2>
    <p>Remaining: {{ remaining }}</p>
 
    <form @submit.prevent="addTask">
      <input
        v-model="draft"
        type="text"
        placeholder="Type a task and press Enter"
        @keydown.enter.prevent="addTask"
      />
      <button type="submit">Add</button>
    </form>
 
    <ul>
      <li v-for="task in tasks" :key="task.id">
        <button @click="toggleTask(task.id)">
          {{ task.done ? "Reopen" : "Complete" }}
        </button>
        <span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
          {{ task.title }}
        </span>
      </li>
    </ul>
  </section>
</template>
<script lang="ts">
export default {
  data() {
    return {
      draft: "",
      tasks: [],
    };
  },
  computed: {
    remaining() {
      return this.tasks.filter((task) => !task.done).length;
    },
  },
  methods: {
    addTask() {
      const title = this.draft.trim();
      if (!title) return;

      this.tasks.push({
        id: Date.now(),
        title,
        done: false,
      });
      this.draft = "";
    },
    toggleTask(id) {
      const task = this.tasks.find((item) => item.id === id);
      if (!task) return;
      task.done = !task.done;
    },
  },
};
</script>
 
<template>
  <section>
    <h2>Tasks</h2>
    <p>Remaining: {{ remaining }}</p>
 
    <form @submit.prevent="addTask">
      <input
        v-model="draft"
        type="text"
        placeholder="Type a task and press Enter"
        @keydown.enter.prevent="addTask"
      />
      <button type="submit">Add</button>
    </form>
 
    <ul>
      <li v-for="task in tasks" :key="task.id">
        <button @click="toggleTask(task.id)">
          {{ task.done ? "Reopen" : "Complete" }}
        </button>
        <span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
          {{ task.title }}
        </span>
      </li>
    </ul>
  </section>
</template>

Summary

v-on is the directive that turns static templates into interactive interfaces. If you use it with clear handlers and the right modifiers, you reduce bugs and improve readability.

Key points to remember:

  • Use @ as shorthand for v-on.
  • Prefer script functions over complex inline expressions.
  • Use modifiers (.prevent, .stop, .once) when needed.
  • Keep behavior consistent across Composition API and Options API for easier maintenance.
Edit this page on GitHub

Found an issue or want to improve this post? You can propose changes directly.