- Vue.js
Vue @clickイベント修飾子一覧|.prevent .stop .once
Vueのイベント修飾子とは、@click.prevent のようにイベント名の後ろへドットで付け足し、preventDefault や stopPropagation などの定型処理をテンプレート側に書ける仕組みです。 ハンドラーの中身を「やりたい処理」だけに保てるので、コードが読みやすくなります。
この記事では、イベント修飾子・キー修飾子・システム修飾子・マウスボタン修飾子を1枚の表にまとめ、それぞれが内部で行うネイティブ処理を並べました。さらに @click.prevent.self と @click.self.prevent の違い、.exact を付けたときと付けないときの違いを、クリックログの実行結果で示します。引数と $event を同時に渡す書き方、TypeScript での型付け、Vue 3 でなくなった keyCode 修飾子と .native の置き換えまで、すべて動かした結果を載せています。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Vue 3.5.43 / Vitest 5.0.3 + jsdom 30.1.1 + @vue/test-utils 2.5.1 / vue-tsc 3.3.12(TypeScript 6.0.3)。クリックやキー入力は jsdom 上でイベントを発行して確かめています。
Vueイベント修飾子の早見表
| 種類 | 修飾子 | 対応するネイティブ処理 | 使いどころ |
|---|---|---|---|
| イベント | .prevent |
event.preventDefault() |
フォーム送信やリンク遷移など、既定の動作を止める |
| イベント | .stop |
event.stopPropagation() |
親要素へのバブリングを止める |
| イベント | .self |
event.target !== event.currentTarget なら何もしない |
子要素のクリックを除き、その要素自身のときだけ動かす |
| イベント | .once |
addEventListener(…, { once: true }) |
1回だけ受け取る |
| イベント | .capture |
addEventListener(…, { capture: true }) |
子より先に親で受け取る |
| イベント | .passive |
addEventListener(…, { passive: true }) |
スクロールやタッチで既定動作を待たせない |
| キー | .enter .tab .esc .space |
event.key が Enter / Tab / Escape / 空白 |
入力欄の確定、閉じる操作 |
| キー | .delete |
event.key が Delete または Backspace |
削除操作(2つのキーの両方に反応) |
| キー | .up .down .left .right |
event.key が ArrowUp / ArrowDown / ArrowLeft / ArrowRight |
候補リストの移動 |
| キー | .page-down など任意のキー名 |
event.key をケバブケースにした名前と比較 |
エイリアスのないキーを指定する |
| システム | .ctrl .shift .alt .meta |
event.ctrlKey などが false なら何もしない |
Ctrl + クリック、Ctrl + Enter |
| システム | .exact |
指定していないシステムキーが押されていたら何もしない | 「Ctrl だけ」「何も押さずに」を厳密に判定 |
| マウス | .left |
event.button !== 0 なら何もしない |
主ボタンだけに反応 |
| マウス | .right |
contextmenu イベントに置き換え、event.button !== 2 なら何もしない |
右クリックメニュー |
| マウス | .middle |
mouseup イベントに置き換え、event.button !== 1 なら何もしない |
中ボタン |
「何もしない」は、ハンドラーを呼ばずに終わるという意味です。ネイティブ処理の列は、Vue 3.5.43 の runtime-dom に入っている実装と、後半に載せるコンパイル結果で確かめた内容です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue @clickイベント修飾子一覧|.prevent .stop .once」
@clickの基本:ハンドラーの書き方は3通り
@click は v-on:click の省略記法です。値の書き方は次の3通りがあります。
| 書き方 | 例 | 受け取るもの |
|---|---|---|
| メソッド名だけ | @click="save" |
第1引数に DOM イベントが入る |
| インラインの式 | @click="count++"、@click="remove(7)" |
式がそのまま実行される。イベントは $event で参照 |
| アロー関数 | @click="(event) => remove(7, event)" |
引数でイベントを受け取る |
メソッド名だけを書くと、Vue がイベントオブジェクトを第1引数として渡します。 かっこを付けて remove(7) と書いた時点でインラインの式になり、イベントは自動では渡りません。必要なときは $event を自分で渡します(後述)。
イベント修飾子を1つずつ動かす
まず、この記事の検証に使ったコンポーネントの全文です。log という emit を通じて、どのハンドラーが呼ばれたかを外に知らせます。
<!-- BasicDemo.vue -->
<script setup lang="ts">
const emit = defineEmits<{ log: [msg: string] }>()
const log = (m: string) => emit('log', m)
function remove(id: number, label: string, event: MouseEvent) {
const el = event.currentTarget as HTMLButtonElement
log(`remove id=${id} label=${label} type=${event.type} tag=${el.tagName}`)
}
function onSubmit(event: Event) {
log(`submit defaultPrevented=${event.defaultPrevented}`)
}
function onEnter(event: KeyboardEvent) {
log(`keyup key=${event.key}`)
}
</script>
<template>
<!-- 1. バブリング: 子 → 親 -->
<div id="outer1" @click="log('親 div')">
<button id="plain" @click="log('子 button')">そのまま</button>
</div>
<!-- 2. .stop -->
<div id="outer2" @click="log('親 div')">
<button id="stop" @click.stop="log('子 button (.stop)')">止める</button>
</div>
<!-- 3. .capture -->
<div id="outer3" @click.capture="log('親 div (.capture)')">
<button id="cap" @click="log('子 button')">キャプチャ</button>
</div>
<!-- 4. .once -->
<button id="once" @click.once="log('.once')">1回だけ</button>
<!-- 5. 引数と $event -->
<button id="arg1" @click="remove(7, 'ノート', $event)">$event</button>
<button id="arg2" @click="(e) => remove(8, 'ペン', e)">アロー関数</button>
<!-- 6. 複数ハンドラー -->
<button id="multi" @click="log('1つ目'), log('2つ目')">2つ呼ぶ</button>
<!-- 7. フォーム -->
<form id="form" @submit.prevent="onSubmit"><button type="submit">送信</button></form>
<!-- 8. キー -->
<input id="k1" @keyup.enter="onEnter" />
<input id="k2" @keyup.page-down="onEnter" />
<input id="k3" @keyup.ctrl.enter="onEnter" />
<input id="k4" @keyup.esc="onEnter" />
<input id="k5" @keyup.delete="onEnter" />
<!-- 9. マウスボタン -->
<div id="m1" @click.right.prevent="log('右ボタン')">右</div>
<div id="m2" @click.middle="log('中ボタン')">中</div>
</template>
これを Vitest と jsdom でマウントし、イベントを発行してログを集めます。
// basic.test.ts
import { test } from 'vitest'
import { mount } from '@vue/test-utils'
import BasicDemo from './BasicDemo.vue'
test('基本の修飾子', async () => {
const logs: string[] = []
const w = mount(BasicDemo, { attachTo: document.body, props: { onLog: (m: string) => logs.push(m) } })
const show = (label: string) => { console.log(`${label} → ${logs.join(' / ') || '(なし)'}`); logs.length = 0 }
const click = (id: string) => w.find(id).element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
click('#plain'); show('修飾子なし')
click('#stop'); show('.stop')
click('#cap'); show('.capture')
click('#once'); click('#once'); click('#once'); show('.once を3回クリック')
click('#arg1'); show('引数 + $event')
click('#arg2'); show('引数 + アロー関数')
click('#multi'); show('カンマ区切り')
w.find('#form').element.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); show('@submit.prevent')
const key = (id: string, init: KeyboardEventInit) => w.find(id).element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, ...init }))
key('#k1', { key: 'Enter' }); show('.enter に Enter')
key('#k1', { key: 'a' }); show('.enter に a')
key('#k2', { key: 'PageDown' }); show('.page-down に PageDown')
key('#k3', { key: 'Enter' }); show('.ctrl.enter に Enter だけ')
key('#k3', { key: 'Enter', ctrlKey: true }); show('.ctrl.enter に Ctrl+Enter')
key('#k4', { key: 'Escape' }); show('.esc に Escape')
key('#k5', { key: 'Backspace' }); show('.delete に Backspace')
key('#k5', { key: 'Delete' }); show('.delete に Delete')
const cm = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, button: 2 })
w.find('#m1').element.dispatchEvent(cm); show(`.right (contextmenu, defaultPrevented=${cm.defaultPrevented})`)
w.find('#m2').element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, button: 1 })); show('.middle (mouseup button=1)')
w.find('#m2').element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, button: 0 })); show('.middle (mouseup button=0)')
})
設定ファイルは次の4行です。
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom' } })
実行コマンドは npx vitest run --silent=false --reporter=verbose です。出力されたログは次のとおりでした。
修飾子なし → 子 button / 親 div
.stop → 子 button (.stop)
.capture → 親 div (.capture) / 子 button
.once を3回クリック → .once
引数 + $event → remove id=7 label=ノート type=click tag=BUTTON
引数 + アロー関数 → remove id=8 label=ペン type=click tag=BUTTON
カンマ区切り → 1つ目 / 2つ目
@submit.prevent → submit defaultPrevented=true
.enter に Enter → keyup key=Enter
.enter に a → (なし)
.page-down に PageDown → keyup key=PageDown
.ctrl.enter に Enter だけ → (なし)
.ctrl.enter に Ctrl+Enter → keyup key=Enter
.esc に Escape → keyup key=Escape
.delete に Backspace → keyup key=Backspace
.delete に Delete → keyup key=Delete
.right (contextmenu, defaultPrevented=true) → 右ボタン
.middle (mouseup button=1) → 中ボタン
.middle (mouseup button=0) → (なし)
.stop:親へ伝えない
修飾子なしでは「子 button / 親 div」と2つ呼ばれ、.stop を付けると子だけになりました。クリックは子から親へ順に伝わる(バブリング)ので、親のハンドラーを動かしたくないときに .stop を付けます。 一覧の行全体にクリックを付け、行の中の削除ボタンだけ別の動きにしたい、という場面でよく使います。
.prevent:既定の動作を止める
@submit.prevent のログでは defaultPrevented=true になっています。ハンドラーが呼ばれた時点で、すでに preventDefault が済んでいるということです。フォームのページ再読み込みを止める @submit.prevent は、最もよく使う組み合わせです。
値を書かずに @submit.prevent だけでも使えます。この場合は「既定の動作を止めるだけ」のリスナーになります。
.capture と .once と .passive:リスナーのオプションになる3つ
.capture を付けた親は、子より先に呼ばれました(「親 div (.capture) / 子 button」)。.once を付けたボタンは、3回クリックしてもログは1回だけです。
この3つは他の修飾子と仕組みが異なり、addEventListener の第3引数(オプション)として登録されます。 .passive はスクロールやタッチ系のイベントで、ブラウザーに「preventDefault を呼ばない」と先に伝えるためのものです。Vue の公式ガイドには、.passive と .prevent を一緒に使わないこと、一緒に使うとブラウザーが警告を出す可能性があることが書かれています。
.self:その要素自身のクリックだけ
.self は「クリックされた要素(event.target)が、リスナーを付けた要素(event.currentTarget)と同じとき」だけハンドラーを呼びます。モーダルの背景に @click.self="open = false" と書くと、背景のクリックでは閉じ、中のパネルをクリックしても閉じない動きになります(この形でも動作を確かめました)。
修飾子の順序で結果が変わる:.prevent.self と .self.prevent
修飾子は複数つなげられます。このとき、書いた順番に処理されるので、順序によって結果が変わります。 公式ガイドにも「関連するコードが同じ順番で生成されるため、修飾子を使用するときには順番は重要です」とあります。
リンクの中に子要素を置いた、次のコンポーネントで確かめます。
<!-- OrderDemo.vue -->
<script setup lang="ts">
const emit = defineEmits<{ log: [msg: string] }>()
function onA() { emit('log', 'A: ハンドラー実行') }
function onB() { emit('log', 'B: ハンドラー実行') }
</script>
<template>
<!-- A: prevent → self の順 -->
<a id="a" href="/next" @click.prevent.self="onA">
<span id="a-child">A の子要素</span>
</a>
<!-- B: self → prevent の順 -->
<a id="b" href="/next" @click.self.prevent="onB">
<span id="b-child">B の子要素</span>
</a>
</template>
basic.test.ts と同じ形のテストで、#a・#a-child・#b・#b-child の順に click を発行し、そのつど event.defaultPrevented と呼ばれたハンドラーを出力しました。
クリックログは次のとおりです。
#a をクリック → defaultPrevented=true / A: ハンドラー実行
#a-child をクリック → defaultPrevented=true / ハンドラーは呼ばれない
#b をクリック → defaultPrevented=true / B: ハンドラー実行
#b-child をクリック → defaultPrevented=false / ハンドラーは呼ばれない
Not implemented: navigation to another Document
最後の1行は jsdom が出したメッセージです。jsdom はページ遷移を持たないため、リンクの既定動作(遷移)が始まったところでこの1行を表示します。つまり #b-child のクリックでは、リンクの遷移が止まっていません。
| 書き方 | リンク自身をクリック | 子要素をクリック |
|---|---|---|
@click.prevent.self |
遷移を止める+ハンドラー実行 | 遷移を止める(ハンドラーは呼ばれない) |
@click.self.prevent |
遷移を止める+ハンドラー実行 | 遷移する(ハンドラーは呼ばれない) |
.prevent.self は「子要素も含めてすべてのクリックで遷移を止める」、.self.prevent は「要素自身のクリックだけ遷移を止める」です。
理由:修飾子は左から順にチェックされる
なぜこうなるかは、コンパイル結果と Vue 本体の実装を見るとはっきりします。テンプレートをコンパイルした結果は次のとおりです(@vue/compiler-dom の compile 関数で出力)。
@click.prevent.self="go" → { onClick: _withModifiers(_ctx.go, ["prevent","self"]) }
@click.self.prevent="go" → { onClick: _withModifiers(_ctx.go, ["self","prevent"]) }
修飾子が配列として、書いた順のまま渡されています。受け取る側の withModifiers は、Vue 3.5.43 の runtime-dom で次のように実装されています。
const systemModifiers = ["ctrl", "shift", "alt", "meta"];
const modifierGuards = {
stop: (e) => e.stopPropagation(),
prevent: (e) => e.preventDefault(),
self: (e) => e.target !== e.currentTarget,
ctrl: (e) => !e.ctrlKey,
shift: (e) => !e.shiftKey,
alt: (e) => !e.altKey,
meta: (e) => !e.metaKey,
left: (e) => "button" in e && e.button !== 0,
middle: (e) => "button" in e && e.button !== 1,
right: (e) => "button" in e && e.button !== 2,
exact: (e, modifiers) => systemModifiers.some((m) => e[`${m}Key`] && !modifiers.includes(m))
};
const withModifiers = (fn, modifiers) => {
if (!fn) return fn;
const cache = fn._withMods || (fn._withMods = {});
const cacheKey = modifiers.join(".");
return cache[cacheKey] || (cache[cacheKey] = ((event, ...args) => {
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]];
if (guard && guard(event, modifiers)) return;
}
return fn(event, ...args);
}));
};
for 文で修飾子を左から1つずつ処理し、どれかが true を返した時点で return して終わります。 .self.prevent では、子要素のクリックのとき最初の self が true を返すので、次の prevent まで進みません。.prevent.self では先に preventDefault が済んでから self で止まります。
同じ考え方で、.stop と .self の順序も結果が変わります。@click.self.stop は「自身のクリックだけバブリングを止める」、@click.stop.self は「子のクリックも含めてバブリングを止める」です。迷ったら「左から順に実行され、条件に合わなければそこで終わる」と読み下すと判断できます。
.exactの有無で変わること:Ctrl+クリックの判定
システム修飾子(.ctrl .shift .alt .meta)は、「そのキーが押されていること」だけを見ます。 他のキーが一緒に押されていても反応します。「そのキーだけ」に限定したいときに付けるのが .exact です。
<!-- ExactDemo.vue -->
<script setup lang="ts">
const emit = defineEmits<{ log: [msg: string] }>()
</script>
<template>
<button id="b1" @click="emit('log', '@click')">1</button>
<button id="b2" @click.ctrl="emit('log', '@click.ctrl')">2</button>
<button id="b3" @click.ctrl.exact="emit('log', '@click.ctrl.exact')">3</button>
<button id="b4" @click.exact="emit('log', '@click.exact')">4</button>
<button id="b5" @click.ctrl.shift="emit('log', '@click.ctrl.shift')">5</button>
</template>
5つのボタンすべてに、ctrlKey や shiftKey を変えた click を発行した結果です。
キーなし でクリック → @click , @click.exact
Ctrl でクリック → @click , @click.ctrl , @click.ctrl.exact
Ctrl+Shift でクリック → @click , @click.ctrl , @click.ctrl.shift
Shift でクリック → @click
表に直すと次のようになります。
| 書き方 | キーなし | Ctrl | Ctrl+Shift | Shift |
|---|---|---|---|---|
@click |
反応 | 反応 | 反応 | 反応 |
@click.ctrl |
- | 反応 | 反応 | - |
@click.ctrl.exact |
- | 反応 | - | - |
@click.exact |
反応 | - | - | - |
@click.ctrl.shift |
- | - | 反応 | - |
@click.ctrlは Ctrl+Shift でも反応します。 「Ctrl だけ」にしたいときは.ctrl.exactにします。@click.exactは、システムキーを何も押していないクリックだけに反応します。 通常のクリックと Ctrl + クリックで動きを分けたいとき、通常側に.exactを付けると重なりません。- 修飾子なしの
@clickは、どのキーを押していても反応します。
公式ガイドによると、.meta は Mac のキーボードでは command キー、Windows のキーボードでは Windows キーに対応します。また、システム修飾子を keyup と組み合わせる場合、イベントが発行される時点でそのキーが押されている必要があります。つまり @keyup.ctrl は、Ctrl を押したまま別のキーを離したときに反応します。
キー修飾子:Enterや矢印キーを指定する
@keyup.enter のように、キーボードイベントにはキー名を修飾子として付けられます。よく使うキーには短いエイリアスが用意されています。
| エイリアス | 比較される event.key | 実行ログでの確認 |
|---|---|---|
.enter |
Enter | 「.enter に Enter」で反応、「.enter に a」は反応なし |
.tab |
Tab | - |
.delete |
Delete / Backspace | Backspace と Delete の両方で反応 |
.esc |
Escape | 「.esc に Escape」で反応 |
.space |
空白文字 | - |
.up .down .left .right |
ArrowUp / ArrowDown / ArrowLeft / ArrowRight | - |
エイリアスにないキーは、KeyboardEvent.key の値をケバブケース(小文字とハイフン)にした名前で指定します。PageDown なら .page-down です。先ほどのログでも「.page-down に PageDown」で反応しています。
システム修飾子と組み合わせると、ショートカットキーも書けます。@keyup.ctrl.enter は、Enter だけでは反応せず、Ctrl + Enter で反応しました。
日本語入力の変換確定で反応させない書き方
入力欄で「Enter で登録」を作るとき、日本語の変換を確定する Enter にも反応すると、変換途中の文字が登録されてしまいます。KeyboardEvent の isComposing(変換中かどうか)を見て、変換中は処理を見送ると落ち着いた動きになります。
<!-- ImeDemo.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const text = ref('')
const items = ref<string[]>([])
function add(event: KeyboardEvent) {
if (event.isComposing) return // 変換を確定する Enter は見送る
if (text.value.trim() === '') return
items.value.push(text.value.trim())
text.value = ''
}
</script>
<template>
<input v-model="text" placeholder="タスクを入力して Enter" @keydown.enter="add" />
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</template>
isComposing=true の Enter → 登録数 0
isComposing=false の Enter → 登録数 1
isComposing を true・false にした keydown を順に発行した結果です。isComposing が true の keydown では登録されず、false の keydown で1件登録されました。ここでは isComposing の値を指定したイベントを jsdom 上で発行して、分岐が働くことを確かめています。
マウスボタン修飾子:.left .right .middle
マウスボタン修飾子は、event.button の値でボタンを見分けます。コンパイル結果を見ると、.right と .middle は受け取るイベントそのものが置き換わることがわかります。
@click.left="go" → { onClick: _withModifiers(_ctx.go, ["left"]) }
@click.right="go" → { onContextmenu: _withModifiers(_ctx.go, ["right"]) }
@click.middle="go" → { onMouseup: _withModifiers(_ctx.go, ["middle"]) }
@keyup.right="go" → { onKeyup: _withKeys(_ctx.go, ["right"]) }
| 書き方 | 実際に登録されるイベント | 判定 |
|---|---|---|
@click.left |
click | button が 0 |
@click.right |
contextmenu | button が 2 |
@click.middle |
mouseup | button が 1 |
@click.rightは contextmenu イベントに置き換わります。 ブラウザーの右クリックメニューを出したくないときは@click.right.preventと書きます。実行ログでもdefaultPrevented=trueになりました。@click.middleは mouseup に置き換わり、button が 1 のときだけ反応します(button が 0 の mouseup では反応なし)。.leftと.rightは、キーボードイベントでは矢印キーの意味になります。@keyup.rightは右矢印キー、@click.rightは右ボタンです。同じ名前でも、付けるイベントで意味が切り替わります。
公式ガイドには、.left .right .middle という名前は一般的な右利き用マウスの配置にもとづくもので、実際には「メイン」「セカンダリ」「補助」のポインティングデバイスのイベントを表す、と説明されています。
引数と$eventを同時に渡す
「どの行が押されたか」という ID と、イベントオブジェクトの両方を受け取りたい場面はよくあります。書き方は2つです。
<!-- ArgDemo.vue -->
<script setup lang="ts">
const items = [
{ id: 7, label: 'ノート' },
{ id: 8, label: 'ペン' },
]
function remove(id: number, label: string, event: MouseEvent) {
const el = event.currentTarget as HTMLButtonElement
console.log(`remove id=${id} label=${label} type=${event.type} tag=${el.tagName}`)
}
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.label }}
<!-- 書き方1: 特別な変数 $event を渡す -->
<button @click="remove(item.id, item.label, $event)">削除</button>
<!-- 書き方2: アロー関数の引数で受け取る -->
<button @click="(event) => remove(item.id, item.label, event)">削除</button>
</li>
</ul>
</template>
先ほどのログでは、どちらの書き方でも ID・ラベル・イベントの3つが届いていました。
引数 + $event → remove id=7 label=ノート type=click tag=BUTTON
引数 + アロー関数 → remove id=8 label=ペン type=click tag=BUTTON
| やりたいこと | 書き方 |
|---|---|
| イベントだけ受け取る | @click="onClick"(第1引数に入る) |
| 引数だけ渡す | @click="remove(item.id)" |
| 引数とイベントの両方 | @click="remove(item.id, $event)" |
| 引数とイベントの両方(アロー関数) | @click="(event) => remove(item.id, event)" |
| 2つの処理を続けて呼ぶ | @click="save(), close()"(カンマで区切る) |
| 修飾子と引数を同時に使う | @click.stop="remove(item.id, $event)" |
$event はテンプレートの中だけで使える特別な変数です。 script 側で $event という名前を参照することはできないので、必ず引数として渡します。修飾子を付けても $event はそのまま使えます。
TypeScriptでイベントに型を付ける
script setup を TypeScript で書くとき、ハンドラーの引数に型を付けると、event.key や event.clientX の補完と型チェックが働きます。
型を付けた完成形
<!-- TypeOk.vue -->
<script setup lang="ts">
function onClick(event: MouseEvent) {
const button = event.currentTarget as HTMLButtonElement
console.log(button.textContent, event.clientX, event.ctrlKey)
}
function onKey(event: KeyboardEvent) {
console.log(event.key, event.ctrlKey)
}
function onInput(event: Event) {
const input = event.target as HTMLInputElement
console.log(input.value)
}
function onSubmit(event: SubmitEvent) {
const form = event.currentTarget as HTMLFormElement
console.log(new FormData(form).get('title'))
}
function remove(id: number, event: MouseEvent) {
console.log(id, event.clientX)
}
</script>
<template>
<button @click="onClick">クリック</button>
<input @keyup.enter="onKey" @input="onInput" />
<form @submit.prevent="onSubmit"><input name="title" /></form>
<button @click="remove(7, $event)">$event</button>
<button @click="(event) => remove(8, event)">アロー関数</button>
</template>
このファイルは npx vue-tsc --noEmit でエラーなしでした。$event もアロー関数の引数も、イベント名に合った型が自動で付きます。 そのため remove(7, $event) の第2引数が MouseEvent であることまでチェックされます。
型が合わないときのエラー文
型を取り違えた例を、あえて vue-tsc に通してみます。
<!-- TypeNg.vue -->
<script setup lang="ts">
function onKey(event: KeyboardEvent) {
console.log(event.key)
}
function onInput(event: Event) {
console.log(event.target.value)
}
function remove(id: number, event: MouseEvent) {
console.log(id, event.clientX)
}
</script>
<template>
<button @click="onKey">A</button>
<input @input="onInput" />
<button @click="remove('7', $event)">B</button>
<input @keyup.enter="remove(7, $event)" />
</template>
a1/TypeNg.vue(6,15): error TS18047: 'event.target' is possibly 'null'.
a1/TypeNg.vue(6,28): error TS2339: Property 'value' does not exist on type 'EventTarget'.
a1/TypeNg.vue(14,4): error TS2345: Argument of type '{ onClick: (event: KeyboardEvent) => void; }' is not assignable to parameter of type 'ButtonHTMLAttributes & ReservedProps & Record<string, unknown>'.
Type '{ onClick: (event: KeyboardEvent) => void; }' is not assignable to type 'ButtonHTMLAttributes'.
Types of property 'onClick' are incompatible.
Type '(event: KeyboardEvent) => void' is not assignable to type '(payload: PointerEvent) => void'.
Types of parameters 'event' and 'payload' are incompatible.
Type 'PointerEvent' is missing the following properties from type 'KeyboardEvent': charCode, code, isComposing, key, and 8 more.
a1/TypeNg.vue(16,26): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
a1/TypeNg.vue(17,34): error TS2345: Argument of type 'KeyboardEvent' is not assignable to parameter of type 'MouseEvent'.
Type 'KeyboardEvent' is missing the following properties from type 'MouseEvent': button, buttons, clientX, clientY, and 14 more.
| エラー | 原因 | 直し方 |
|---|---|---|
| TS18047 / TS2339(event.target) | event.target の型は EventTarget(null の可能性あり)で、value を持たない |
const input = event.target as HTMLInputElement と要素の型を伝える |
| TS2345(onClick に KeyboardEvent) | click に渡るのは PointerEvent なので、KeyboardEvent 用の関数は合わない | 引数を MouseEvent にする |
| TS2345('7' を number へ) | テンプレートの引数も型チェックの対象 | remove(7, $event) と数値で渡す |
| TS2345(keyup の $event を MouseEvent へ) | keyup の $event は KeyboardEvent |
受け取る関数の型を KeyboardEvent にする |
イベントごとの型の対応表
Vue 3.5.43 の型定義で、各イベントのハンドラーに渡る型を vue-tsc のエラー文から読み取った結果です。
| イベント | 渡される型 | 引数に書ける型の例 |
|---|---|---|
@click @contextmenu @pointerdown |
PointerEvent | MouseEvent でも受け取れる(PointerEvent は MouseEvent を継承) |
@dblclick @mousedown @mouseup @mouseenter |
MouseEvent | MouseEvent |
@keydown @keyup |
KeyboardEvent | KeyboardEvent |
@input |
InputEvent | Event でも受け取れる |
@change @scroll |
Event | Event |
@focus @blur |
FocusEvent | FocusEvent |
@submit |
SubmitEvent | SubmitEvent、Event |
@paste |
ClipboardEvent | ClipboardEvent |
クリックは MouseEvent、キー入力は KeyboardEvent と覚えておけば、ほとんどの場面で足ります。 要素を取り出すときは、リスナーを付けた要素を指す event.currentTarget を as HTMLButtonElement のように絞り込むと、補完が効いて書きやすくなります。
Vue 3でなくなった修飾子と置き換え表
Vue 2 のコードを Vue 3 へ移すときに書き換えが必要な修飾子は、次の3つです。
| Vue 2 の書き方 | Vue 3 での扱い | 置き換え |
|---|---|---|
@keyup.13(数値の keyCode) |
サポート終了 | @keyup.enter などキー名にする |
Vue.config.keyCodes で独自エイリアス |
サポート終了 | @keyup.page-down のようにケバブケースのキー名にする |
@click.native(コンポーネントに DOM イベントを付ける) |
修飾子が削除 | .native を消す。子コンポーネントは emits に発行するイベントを宣言する |
Vue 3 移行ガイドによると、句読点のキーはそのまま修飾子に書けます(カンマなら @keyup.,)。一方、引用符やスラッシュなど構文上書けない文字は、ハンドラーの中で event.key を調べます。
実行ログで確認する
親コンポーネントから、emits を宣言した子・宣言していない子の両方に @click を付け、あわせて @keyup.13 も試します。
<!-- Child.vue -->
<script setup lang="ts">
defineEmits<{ click: [id: number] }>()
</script>
<template>
<button class="declared" @click="$emit('click', 42)">emits に click を宣言</button>
</template>
<!-- ChildPlain.vue -->
<template>
<button class="plain">emits の宣言なし</button>
</template>
<!-- NativeDemo.vue -->
<script setup lang="ts">
import Child from './Child.vue'
import ChildPlain from './ChildPlain.vue'
const emit = defineEmits<{ log: [msg: string] }>()
function show(name: string, payload: unknown) {
const kind = payload instanceof Event ? `DOMイベント(${payload.type})` : `emit の値(${payload})`
emit('log', `${name}: ${kind}`)
}
</script>
<template>
<Child @click="show('Child', $event)" />
<ChildPlain @click="show('ChildPlain', $event)" />
<input id="code" @keyup.13="emit('log', '@keyup.13')" />
<input id="name" @keyup.enter="emit('log', '@keyup.enter')" />
<input id="comma" @keyup.,="emit('log', '@keyup.,')" />
</template>
2つの子のボタンに click を、3つの入力欄に keyup(Enter とカンマ)を発行した結果です。
emits 宣言ありの子をクリック → Child: emit の値(42)
emits 宣言なしの子をクリック → ChildPlain: DOMイベント(click)
@keyup.13 に Enter → (なし)
@keyup.enter に Enter → @keyup.enter
@keyup., に , → @keyup.,
- emits を宣言していない子では、
@clickがルート要素の DOM イベントとしてそのまま働きます。 Vue 2 で.nativeが担っていた役割を、Vue 3 では修飾子なしで実現できます。 - emits に click を宣言した子では、子が emit した値(42)だけが届きます。 DOM のクリックと二重に届くことはありません。コンポーネントが発行するイベントを emits に書いておくと、この切り分けが働きます。
@keyup.13は Enter を押しても反応しません。 エラーにも警告にもならず静かに無反応になるので、移行時は数値の修飾子を検索して置き換えると確実です。
なお、@click.native と書いたテンプレートを Vue 3.5.43 でコンパイルすると、次のように .native の部分は何も生成されませんでした。
@click.native="go" → { onClick: _ctx.go }
修飾子がコンパイルされた結果の一覧
修飾子の仕組みは、コンパイル結果を並べると整理しやすくなります。@vue/compiler-dom の compile 関数に compile('<div @click.stop="go"></div>', { mode: 'module' }) の形で属性を1つずつ渡し、生成されたコードから props の部分を抜き出しました。
@click="go" → { onClick: _ctx.go }
@click.stop="go" → { onClick: _withModifiers(_ctx.go, ["stop"]) }
@click.once="go" → { onClickOnce: _ctx.go }
@click.capture="go" → { onClickCapture: _ctx.go }
@scroll.passive="go" → { onScrollPassive: _ctx.go }
@click.capture.once="go" → { onClickCaptureOnce: _ctx.go }
@keyup.enter="go" → { onKeyup: _withKeys(_ctx.go, ["enter"]) }
@keyup.page-down="go" → { onKeyup: _withKeys(_ctx.go, ["page-down"]) }
@keyup.ctrl.enter="go" → { onKeyup: _withKeys(_withModifiers(_ctx.go, ["ctrl"]), ["enter"]) }
@keyup.13="go" → { onKeyup: _withKeys(_ctx.go, ["13"]) }
@click.ctrl.exact="go" → { onClick: _withModifiers(_ctx.go, ["ctrl","exact"]) }
@click.exact="go" → { onClick: _withModifiers(_ctx.go, ["exact"]) }
@click.prevent → { onClick: _withModifiers(() => {}, ["prevent"]) }
この出力から、修飾子は3つのグループに分かれることが読み取れます。
| グループ | 修飾子 | コンパイル後の形 |
|---|---|---|
| リスナーのオプションになる | .once .capture .passive |
プロパティ名の末尾に付く(onClickOnce、onClickCapture など) |
| ハンドラーの前でチェックする | .stop .prevent .self .ctrl .shift .alt .meta .exact .left .right .middle |
withModifiers で包む |
| キー名を比較する | .enter .page-down など |
withKeys で包む |
@keyup.13 は withKeys に文字列の "13" が渡っています。Vue 3 は event.key(Enter のようなキーの名前)と比較するので、"13" とは一致せず、反応しなくなるというわけです。
つまずきやすい箇所と直し方
| 起きること | 理由 | 直し方 |
|---|---|---|
| 子要素をクリックするとリンクが遷移する | .self.prevent の順だと、子のクリックでは prevent まで進まない |
.prevent.self の順にする |
| Ctrl+Shift+クリックでも Ctrl 用の処理が動く | システム修飾子は「押されていること」だけを見る | .ctrl.exact にする |
| 通常クリックと Ctrl+クリックの両方が動く | 修飾子なしの @click はどのキーでも反応する |
通常側を @click.exact にする |
remove(item.id) の中でイベントを使えない |
かっこを付けるとイベントは自動で渡らない | remove(item.id, $event) と渡す |
event.target.value が型エラーになる |
event.target は EventTarget 型 |
event.target as HTMLInputElement と絞り込む |
@keyup.13 が反応しない |
Vue 3 はキー名で比較する | @keyup.enter にする |
| 変換確定の Enter で登録される | keydown は変換中にも発行される | event.isComposing が true のときは return する |
| 右クリックでブラウザーのメニューも出る | .right は contextmenu を受け取るだけ |
@click.right.prevent にする |
コンポーネントに付けた @click に emit の値が届く |
子が emits に click を宣言している | DOM イベントが欲しい場合は、子の中の要素から必要な情報を emit する |
よくある質問
Q. .prevent と .stop はどう違いますか?
A. .prevent は「ブラウザーの既定の動作(フォーム送信、リンク遷移、右クリックメニューなど)を止める」、.stop は「親要素へイベントが伝わるのを止める」です。目的が別なので、両方必要なら @click.stop.prevent のように並べて書きます。
Q. 修飾子はいくつまでつなげられますか?
A. 個数の上限は特にありません。大切なのは順序で、左から順に処理され、条件に合わなかった時点でそこで終わります。.prevent や .stop のように必ず実行したい処理は左に、.self や .ctrl のように条件を絞るものは右に置くと、意図どおりになりやすいです。
Q. @keyup と @keydown はどちらを使えばよいですか? A. キーを押した瞬間に反応させたい、または押しっぱなしで繰り返したいときは keydown、キーを離したときに1回だけ反応させたいときは keyup です。入力欄の Enter で登録する処理は、isComposing の判定と組み合わせて keydown に付けると、日本語入力でも落ち着いて動きます。
まとめ
- イベント修飾子は6つ(
.prevent.stop.self.once.capture.passive)。.preventは preventDefault、.stopは stopPropagation に対応する - 修飾子は左から順に処理される。
.prevent.selfは子のクリックでも遷移を止め、.self.preventは要素自身のクリックだけ止める - システム修飾子は「押されていること」だけを見る。そのキーだけに限定するなら
.exactを付ける @click.rightは contextmenu、@click.middleは mouseup に置き換わる。.left.rightはキーボードイベントでは矢印キーになる- 引数とイベントを両方渡すには
$eventかアロー関数。TypeScript ではクリックに MouseEvent、キー入力に KeyboardEvent を付ける - Vue 3 では
@keyup.13と.nativeがなくなった。キー名(.enter.page-down)と emits の宣言に置き換える
参考
- Vue.js 公式ガイド「イベントハンドリング」 https://ja.vuejs.org/guide/essentials/event-handling
- Vue 3 移行ガイド(keyCode 修飾子のページ) https://v3-migration.vuejs.org/ja/breaking-changes/keycode-modifiers
- Vue 3 移行ガイド(v-on の .native 修飾子のページ) https://v3-migration.vuejs.org/ja/breaking-changes/v-on-native-modifier-removed
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue @clickイベント修飾子一覧|.prevent .stop .once」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。