- Vue.js
Vue v-modelの使い方|要素別の値の型・修飾子・コンポーネント
v-modelとは、入力欄の値と変数を双方向に結びつけるVueのディレクティブです。 入力すると変数が変わり、変数を変えると入力欄の表示も変わります。フォームの入力要素にも、自作のコンポーネントにも使えます。
この記事では、Vue 3.5.43 で、入力要素ごとに v-model がどんな値(型)になるかを Chromium で実際に入力して確かめた一覧を中心に、修飾子、カスタム修飾子、コンポーネントの v-model までをまとめました。さらに defineModel と従来の props + emit がビルド後にどんなコードになるかの比較、オブジェクトを渡したときの警告、日本語入力の変換中は値が更新されない挙動も、実行結果で確認しています。
v-modelの早見表(要素ごとの値の型)
Chromium で実際に操作し、変数の値と typeof を表示した結果です。
| 要素 | 書き方 | 操作 | 変数に入る値 |
|---|---|---|---|
| テキスト | <input v-model="text"> |
「発注」と入力 | "発注"(string) |
| 複数行 | <textarea v-model="note"> |
2行入力 | "1行目\n2行目"(string) |
| チェックボックス単体 | <input type="checkbox" v-model="agree"> |
チェック | true(boolean) |
| 単体 + 独自の値 | true-value="対応済み" false-value="未対応" |
チェック | "対応済み"(string) |
| チェックボックス複数 | 同じ配列を v-model + value |
2つチェック | ["至急","確認待ち"](array) |
| ラジオボタン | <input type="radio" value="高" v-model="priority"> |
「高」を選択 | "高"(string) |
| セレクト単体 | <select v-model="category"> |
:value にオブジェクト |
{"id":2,"name":"家具"}(object) |
| セレクト複数 | <select v-model="places" multiple> |
2つ選択 | ["倉庫A","倉庫C"](array) |
| 数値入力 | <input type="number" v-model="qty"> |
「5」と入力 | 5(number) |
修飾子の早見表
| 修飾子 | 動き | 例:入力 → 変数 |
|---|---|---|
.lazy |
input ではなく change(確定・フォーカスが外れた時)で更新 |
入力中は ""、外れると "確定前" |
.trim |
前後の空白を削除 | " ノート " → "ノート" |
.number |
数値に変換できれば number にする | "12.5" → 12.5、"12個" → 12、"abc" → "abc" |
カスタム(defineModel) |
子コンポーネントで自由に定義 | v-model.hankaku:"AB123" → "AB123" |
コンポーネントの v-model の早見表
| やりたいこと | 子の書き方(<script setup>) |
親の書き方 |
|---|---|---|
| 1つの値を結ぶ | const model = defineModel() |
<Child v-model="name" /> |
| 名前付きで複数結ぶ | defineModel('title')、defineModel('due') |
v-model:title="t" v-model:due="d" |
| 修飾子を受け取る | const [model, modifiers] = defineModel({ set(v) {...} }) |
v-model.hankaku="code" |
| Vue 3.3 以前の書き方 | props.modelValue + emit('update:modelValue', v) |
<Child v-model="name" /> |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue v-modelの使い方|要素別の値の型・修飾子・コンポーネント」
検証環境と検証用コード
| 項目 | バージョン |
|---|---|
| Node.js | 22.22.2 |
| vue | 3.5.43 |
| vite / @vitejs/plugin-vue | 8.3.1 / 6.0.9 |
| 画面確認 | Playwright 1.56.1 + Chromium |
npm create vue@latest か、npm i vue と npm i -D vite @vitejs/plugin-vue で作ったプロジェクトに、次のコンポーネントを置けば動きます。
フォーム要素ごとの v-model(コピペ用)
<!-- src/Elements.vue -->
<script setup>
import { ref } from 'vue'
const text = ref('')
const note = ref('')
const agree = ref(false)
const status = ref('未対応')
const tags = ref([]) // 複数チェックボックスは配列で用意する
const priority = ref('')
const category = ref('')
const places = ref([]) // multiple のセレクトも配列
const qty = ref(1)
const lazy = ref('')
const trimmed = ref('')
const num = ref(0)
const show = (v) => `${JSON.stringify(v)} (${Array.isArray(v) ? 'array' : typeof v})`
</script>
<template>
<input id="text" v-model="text" />
<textarea id="note" v-model="note"></textarea>
<input id="agree" type="checkbox" v-model="agree" />
<input id="status" type="checkbox" v-model="status" true-value="対応済み" false-value="未対応" />
<label><input class="tag" type="checkbox" value="至急" v-model="tags" />至急</label>
<label><input class="tag" type="checkbox" value="確認待ち" v-model="tags" />確認待ち</label>
<label><input class="prio" type="radio" value="高" v-model="priority" />高</label>
<label><input class="prio" type="radio" value="低" v-model="priority" />低</label>
<select id="category" v-model="category">
<option disabled value="">選択してください</option>
<option>文具</option>
<option :value="{ id: 2, name: '家具' }">家具</option>
</select>
<select id="places" v-model="places" multiple>
<option>倉庫A</option><option>倉庫B</option><option>倉庫C</option>
</select>
<input id="qty" type="number" v-model="qty" />
<input id="lazy" v-model.lazy="lazy" />
<input id="trim" v-model.trim="trimmed" />
<input id="num" v-model.number="num" />
<pre id="out">text: {{ show(text) }}
note: {{ show(note) }}
agree: {{ show(agree) }}
status: {{ show(status) }}
tags: {{ show(tags) }}
priority: {{ show(priority) }}
category: {{ show(category) }}
places: {{ show(places) }}
qty: {{ show(qty) }}
lazy: {{ show(lazy) }}
trim: {{ show(trimmed) }}
num: {{ show(num) }}</pre>
</template>
実行結果(Chromiumで入力・選択)
--- 初期値
text: "" (string)
note: "" (string)
agree: false (boolean)
status: "未対応" (string)
tags: [] (array)
priority: "" (string)
category: "" (string)
places: [] (array)
qty: 1 (number)
lazy: "" (string)
trim: "" (string)
num: 0 (number)
--- 入力後(lazy はまだフォーカス中)
text: "発注" (string)
note: "1行目\n2行目" (string)
agree: true (boolean)
status: "対応済み" (string)
tags: ["至急","確認待ち"] (array)
priority: "高" (string)
category: {"id":2,"name":"家具"} (object)
places: ["倉庫A","倉庫C"] (array)
qty: 5 (number)
lazy: "" (string)
trim: "ノート" (string)
num: 12.5 (number)
押さえておきたいポイントは次のとおりです。
- チェックボックスは、変数の初期値が配列なら「選んだ value の配列」、それ以外なら true/false になる
true-value/false-valueを付けると、単体のチェックボックスで文字列などを使える(「対応済み」「未対応」のような状態の切り替えに便利)<option>の:valueにオブジェクトを渡すと、オブジェクトのまま受け取れる。IDだけでなく名前も一緒に使いたいときに便利type="number"の入力欄は、修飾子なしでも number になった。Vue 本体のコードでもtype === "number"のときは数値に変換する処理が入っています<select>の初期値に一致する option がないと、iOS では最初の項目を選べなくなることがあるため、公式ドキュメントでは空のvalue=""を持つdisabledの option を置く書き方が紹介されています
修飾子 .lazy / .trim / .number
上のコードの続きで、.lazy の入力欄からフォーカスを外し、.number の欄に別の値を入れた結果です。
--- lazy からフォーカスを外し、num に「12個」
lazy: "確定前" (string)
num: 12 (number)
--- num に「abc」
num: "abc" (string)
.number は parseFloat と同じように先頭から数値として読める部分を使い、読めないときは元の文字列のままにします。そのため「abc」では文字列が入りました。数値だけを受け付けたいなら、.number に頼らず入力チェックを別に書くと確実です。
| 修飾子 | 向いている場面 | 気をつけること |
|---|---|---|
.lazy |
入力のたびに重い処理(検索・保存)が走るのを避けたい | 入力中の文字数カウントなどには不向き |
.trim |
氏名・件名など前後の空白が不要な項目 | 入力欄の表示は change のタイミングで整う |
.number |
数量・金額 | 数値にできない値は文字列のまま |
| 組み合わせ | v-model.lazy.trim="x" |
順番は自由 |
コンポーネントの v-model:defineModel と props + emit
defineModel は Vue 3.4 以降で使える、コンポーネントの v-model を1行で書くためのマクロです。 基本的な使い方は既存記事「Vue 3 Composition APIの使い方」でも扱っているので、ここでは従来の書き方と並べて、ビルド後にどうなるかを中心に見ます。
<!-- src/components/NameInputModel.vue(defineModel 版) -->
<script setup>
const model = defineModel({ type: String, default: '' })
</script>
<template>
<input v-model="model" />
</template>
<!-- src/components/NameInputEmit.vue(従来の props + emit 版) -->
<script setup>
const props = defineProps({ modelValue: { type: String, default: '' } })
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>
<!-- 親:どちらも同じ書き方で使える -->
<NameInputModel v-model="name1" />
<NameInputEmit v-model="name2" />
入力欄に「田中」「鈴木」と入力すると、親の name1: 田中、name2: 鈴木 と、どちらも親の変数が更新されました。
ビルド後の出力を比べる
vite build --minify false でビルドし、出力されたJSから該当部分を抜き出しました。
// defineModel 版(NameInputModel.vue)
var _sfc_main$6 = {
__name: "NameInputModel",
props: {
"modelValue": { type: String, default: "" },
"modelModifiers": {}
},
emits: ["update:modelValue"],
setup(__props) {
const model = useModel(__props, "modelValue");
return (_ctx, _cache) => {
return withDirectives((openBlock(), createElementBlock("input", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => model.value = $event) }, null, 512)), [[vModelText, model.value]]);
};
}
};
// props + emit 版(NameInputEmit.vue)
var _sfc_main$5 = {
__name: "NameInputEmit",
props: { modelValue: { type: String, default: "" } },
emits: ["update:modelValue"],
setup(__props, { emit: __emit }) {
const props = __props;
const emit = __emit;
return (_ctx, _cache) => {
return openBlock(), createElementBlock("input", {
value: props.modelValue,
onInput: _cache[0] || (_cache[0] = ($event) => emit("update:modelValue", $event.target.value))
}, null, 40, _hoisted_1$1);
};
}
};
// 親の <NameInputModel v-model="name1" /> のコンパイル結果
createVNode(_sfc_main$6, {
modelValue: name1.value,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => name1.value = $event)
}, null, 8, ["modelValue"])
出力から読み取れること:
| 比較点 | defineModel | props + emit |
|---|---|---|
| 親に渡る props | modelValue と modelModifiers |
modelValue のみ |
| 親へ送るイベント | update:modelValue(自動で宣言) |
update:modelValue(自分で宣言) |
| 子の中の値 | useModel() が作る ref |
props.modelValue(読み取り専用) |
| 入力欄とのつなぎ | 内部の input にも v-model(vModelText) が使える |
:value + @input を手で書く |
| 修飾子の受け取り | 自動 | modelModifiers を自分で props に書く |
親側のコンパイル結果は、どちらの子でも「modelValue を渡し、onUpdate:modelValue で受け取る」形で同じです。つまり defineModel は、従来の props + emit を自動で書いてくれるマクロだとわかります。Vue 本体の useModel の実装を見ると、親が v-model を付けていない場合は子の中だけで値を保持する処理も入っています。
複数の v-model(v-model:title)
<!-- src/components/TaskTitle.vue -->
<script setup>
const title = defineModel('title', { default: '' })
const due = defineModel('due', { default: '' })
</script>
<template>
<input class="title" v-model="title" placeholder="件名" />
<input class="due" type="date" v-model="due" />
</template>
<!-- 親 -->
<TaskTitle v-model:title="title" v-model:due="due" />
件名に「備品発注(至急)」、日付に 2026-10-15 を入力すると、親は title: 備品発注(至急) / due: 2026-10-15 になりました。コンパイル後は title と onUpdate:title、due と onUpdate:due の組が渡されます。 引数名を付けると modelValue の代わりにその名前が使われる仕組みです。
カスタム修飾子(defineModel の modifiers と set)
defineModel を分割代入すると、2つ目に修飾子のオブジェクトが入ります。 set オプションで、親に送る前の値を変換できます。全角の英数字を半角にする .hankaku を作ってみます。
<!-- src/components/CodeInput.vue -->
<script setup>
const [model, modifiers] = defineModel({
default: '',
set(value) {
if (modifiers.hankaku) {
return value.replace(/[A-Za-z0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
}
return value
},
})
</script>
<template>
<input v-model="model" />
</template>
<!-- 親 -->
<CodeInput v-model.hankaku="code" />
入力「AB123」 → 親の code: AB123 / 入力欄の表示: AB123
親のコンパイル結果には modelModifiers: { hankaku: true } が渡されていました。set で返した値が親に送られ、入力欄の表示も変換後の値になります。 読み出し時に変換したいときは get オプションを使います。名前付きの v-model(v-model:code.hankaku)の場合、修飾子は codeModifiers という props で渡されます。
オブジェクトを v-model で渡すときの注意
props は読み取り専用ですが、「props への代入」と「props のオブジェクトの中身の変更」では結果が違います。 3つの書き方で試しました。
<!-- src/components/MemberEditBad.vue(確認用) -->
<script setup>
const props = defineProps({ modelValue: Object })
function renameDirect() {
props.modelValue = { name: '直接代入' } // props への代入
}
function renameNested() {
props.modelValue.name = '中身だけ変更' // props のオブジェクトの中身を変更
}
</script>
<template>
<button id="bad-direct" @click="renameDirect">propsに代入</button>
<button id="bad-nested" @click="renameNested">中身を変更</button>
</template>
<!-- src/components/MemberEditGood.vue(おすすめ) -->
<script setup>
const member = defineModel({ type: Object, required: true })
function rename(name) {
// 新しいオブジェクトを代入すると update:modelValue が親に届く
member.value = { ...member.value, name }
}
</script>
<template>
<button id="good" @click="rename('新しいオブジェクト')">新しいオブジェクトで更新</button>
</template>
親は const member = ref({ name: '佐藤' }) を v-model="member" で両方に渡しています。実行結果です(開発サーバー)。
[console.warning] [Vue warn] Set operation on key "modelValue" failed: target is readonly. Proxy(Object)
after bad-direct: member: {"name":"佐藤"}
after bad-nested: member: {"name":"中身だけ変更"}
after good: member: {"name":"新しいオブジェクト"}
| 書き方 | 警告 | 親の値 | 評価 |
|---|---|---|---|
props.modelValue = {...} |
出る(target is readonly) |
変わらない | 使えない |
props.modelValue.name = ... |
出ない | 変わる | 親の知らないところで変わるので避ける |
model.value = { ...model.value, name } |
出ない | 変わる(イベント経由) | おすすめ |
中身の変更は警告なしで親の値まで変わってしまうため、気づきにくい点に注意が必要です。子では新しいオブジェクトを作って代入し、update:modelValue で親に知らせると、データの流れが「親 → 子は props、子 → 親はイベント」にそろいます。フォームの項目が多い場合は、項目ごとに v-model:name、v-model:email と分けるのも読みやすい方法です。
日本語入力(IME)中は値が更新されない
v-model を付けたテキスト入力は、日本語を変換している途中(未確定の間)は変数を更新せず、確定したときに更新します。 Chrome DevTools Protocol の Input.imeSetComposition で変換中の状態を再現して確かめました。
変換中「び」: ime: "" / 入力欄: "び"
変換中「びひん」: ime: "" / 入力欄: "びひん"
変換候補「備品」: ime: "" / 入力欄: "備品"
確定後: ime: "備品" / 入力欄: "備品"
ビルド後の Vue 本体のコードにも、次の処理が入っています。
addEventListener(el, lazy ? "change" : "input", (e) => {
if (e.target.composing) return; // 変換中は更新しない
el[assignKey](castValue(el.value, trim, castToNumber));
});
変換中の文字でも検索候補を出したいときは、v-model の代わりに :value と @input で書きます。
<input :value="imeLive" @input="imeLive = $event.target.value" />
変換中: imeLive: "びひん"
確定後: imeLive: "備品"
| 書き方 | 変換中 | 向いている場面 |
|---|---|---|
v-model |
更新しない | 通常のフォーム(確定した値だけ欲しい) |
:value + @input |
更新する | 入力中の候補表示・文字数カウント |
v-model.lazy |
更新しない(change で更新) |
確定後に1回だけ処理したい |
よくあるつまずきと直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| 複数チェックボックスが全部 true/false で連動する | 初期値が配列でない | ref([]) で初期化 |
数量が文字列 "5" になる |
type="text" のまま |
type="number" か .number |
.number なのに文字列が入る |
数値として読めない値 | 別途入力チェック |
子で props.modelValue = ... して警告 |
props は読み取り専用 | defineModel か emit('update:modelValue') |
| オブジェクトの中身が勝手に変わる | 子で props の中身を変更 | 新しいオブジェクトを代入 |
| 変換中の文字で絞り込みたい | v-model は確定まで待つ | :value + @input |
v-model:title が効かない |
子が modelValue 側で受けている |
defineModel('title') にする |
よくある質問
Q. defineModel と props + emit はどちらを使えばよいですか?
A. Vue 3.4 以降なら defineModel がおすすめです。ビルド後の出力のとおり親から見た動きは同じで、子のコードが短くなり、修飾子の受け取りも自動になります。Vue 3.3 以前のプロジェクトや、送る前に独自の処理を細かく書きたい場合は props + emit でも問題ありません。
Q. v-model と :value + @input の違いは何ですか?
A. 入力要素では、v-model は :value と @input をまとめた書き方ですが、日本語の変換中は更新しない処理や、.trim・.number の変換、チェックボックスやセレクトの値の扱いが組み込まれています。変換中の文字も使いたいときだけ :value + @input を使い分けます。
Q. Vue 2 の v-model とは何が違いますか?
A. Vue 2 ではコンポーネントの v-model が value と input イベントを使い、複数の値は .sync 修飾子で扱いました。Vue 3 では modelValue と update:modelValue に変わり、v-model:title のように引数で複数を結べるようになっています。
まとめ
- v-model は入力要素ごとに入る値の型が違う。チェックボックスは 初期値が配列なら配列、それ以外は boolean
type="number"は修飾子なしでも number、.numberは読めない値を文字列のまま残す.lazyは確定時、.trimは前後の空白を削除defineModelはビルドすると props + emit と同じ形になり、modelModifiersまで自動で用意される- カスタム修飾子は
const [model, modifiers] = defineModel({ set })で作れる - オブジェクトは中身を変えず、新しいオブジェクトを代入して親に知らせる
- 日本語の変換中は v-model は更新しない。変換中も使うなら
:value+@input
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue v-modelの使い方|要素別の値の型・修飾子・コンポーネント」
株式会社RJCでは、フォームの細かな挙動を実際に動かして確かめ、その結果をプロジェクトの中で仲間と共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。