Web制作でフォームのJavaScriptを実装していると、「文字を入力した瞬間に処理を動かしたい」「セレクトボックスを選んだ瞬間に切り替えたい」「入力欄から離れたときにバリデーションしたい」など、タイミングに応じたイベントの使い分けが必要になります。

しかし、「change を使ったら文字入力中に動かない」「入力欄を離れたときの判定に change と blur のどちらを使うべきか迷う」「送信ボタンの click だけで処理を書いてよいのか」といった疑問やトラブルはよく起こります。

この記事では、フォーム制御で使われる主要な4つのイベント(input、change、blur、submit)の発火タイミングの違いと、実務での使い分けを解説します。

input・change・blur・submitの違いをまず確認

まずは結論として、4つのイベントの発火タイミングと主な用途を整理します。

イベント 発火するタイミング 主な用途
input 入力内容が変更されるたび(入力中) 文字数カウント、リアルタイム検索、入力中の条件チェック
change 値の変更が確定したとき <select> の選択、チェックボックス・ラジオボタンの切り替え、入力確定後の処理
blur 要素からフォーカスが外れたとき 入力欄を離れた時点での単体バリデーション(形式チェックなど)
submit <form> を送信するとき 送信直前の全体バリデーション、非同期送信(Fetch APIなど)の制御

一言でまとめると、以下の基準で選びます。

  • 入力している途中に動かしたい → input
  • 値の変更を確定させたときに動かしたい → change
  • 変更の有無に関わらず入力欄から離れたときに動かしたい → blur
  • フォーム全体を送信するときに動かしたい → submit

それぞれの挙動と注意点を詳しく見ていきましょう。

inputイベント|入力内容が変わるたびに発火する

input イベントは、ユーザー操作によって要素の value(値)が変更された瞬間に発火します。

テキスト入力欄(<input type="text"> や <textarea>)では、キーボードで1文字入力したり、文字を削除したり、ペーストしたタイミングで連続して発生します。

実装例:リアルタイムな文字数カウント

<form class="form">
  <label for="username">ユーザー名(20文字以内)</label>
  <input type="text" id="username" class="js-name" maxlength="20">
  <p class="js-count">0 / 20文字</p>
</form>
const inputElement = document.querySelector('.js-name');
const countElement = document.querySelector('.js-count');

if (inputElement && countElement) {
  inputElement.addEventListener('input', (event) => {
    const currentLength = event.target.value.length;
    countElement.textContent = `${currentLength} / 20文字`;
  });
}

ユーザーが入力するたびに input イベントが発生し、画面の文字数表示がリアルタイムに更新されます。

注意:JavaScriptでvalueを書き換えてもinputイベントは発火しない

実装時によくあるトラブルの1つが、「JavaScriptプログラムから値を代入したのに、input のリスナーが動かない」というケースです。

const inputElement = document.querySelector('.js-name');

// プログラム側で直接代入する
inputElement.value = '山田太郎';
// この操作では addEventListener('input', ...) は自動で発火しません

ブラウザの仕様上、input や change などのイベントはユーザーがブラウザ上で直接操作したときに発行されます。スクリプトから element.value = '...' のように書き換えた場合はイベントが発生しません。プログラムによる変更時にも連動して処理を実行したい場合は、処理を関数に切り出して直接呼び出すか、new Event('input') を作成して dispatchEvent() で明示的に発行する必要があります。

changeイベント|値の変更が確定したときに発火する

change イベントは、「値の変更が確定したとき」に発火します。

ここで注意が必要なのは、操作するHTML要素の種類によって「確定」とみなされるタイミングが異なるという点です。

1. テキスト入力系(input[type="text"]、textareaなど)

テキスト入力系要素の場合、文字を入力している途中では change は発火しません。

値を変更したあと、入力欄の外をクリックしたりTabキーを押したりして、フォーカスを失った時点で発火します。

const inputElement = document.querySelector('.js-name');

if (inputElement) {
  inputElement.addEventListener('change', (event) => {
    console.log('入力が確定しました:', event.target.value);
  });
}

文字を入力しても、フォーカスがその入力欄にある間は発火せず、別の場所にフォーカスが移動して初めてログが出力されます。

なお、値を一切変更せずにフォーカスを外した場合は change は発火しません。

2. 選択系・切り替え系(select、checkbox、radioなど)

一方、<select> や <input type="checkbox">、<input type="radio"> などの要素では、フォーカスを外す必要はありません。

ユーザーが選択やチェック状態を切り替えた瞬間に発火します。

<form class="form">
  <label for="prefecture">お住まいの地域</label>
  <select id="prefecture" class="js-prefecture">
    <option value="">選択してください</option>
    <option value="aichi">愛知県</option>
    <option value="gifu">岐阜県</option>
    <option value="mie">三重県</option>
  </select>

  <label>
    <input type="checkbox" class="js-agreement"> 利用規約に同意する
  </label>
</form>
const selectElement = document.querySelector('.js-prefecture');
const checkboxElement = document.querySelector('.js-agreement');

if (selectElement) {
  selectElement.addEventListener('change', (event) => {
    console.log('選択された地域:', event.target.value);
  });
}

if (checkboxElement) {
  checkboxElement.addEventListener('change', (event) => {
    console.log('同意状態:', event.target.checked);
  });
}

プルダウンから別の項目を選んだ瞬間や、チェックボックスをクリックしてON/OFFを切り替えた瞬間に即座に change が動きます。

※ラジオボタン(<input type="radio">)では、新しく選択された側で change が発火し、選択が外れた(解除された)側では発火しません。

そのため、「change = フォーカスが外れたときに遅れて動くイベント」と一括りに覚えるのではなく、「値の変更が確定したタイミングで動く(セレクトやチェックボックスは選んだ瞬間に確定する)」と理解しておくことが重要です。

inputとchangeはどう使い分ける?

テキスト入力欄を扱う際、「input と change のどちらを使うべきか」という疑問がよく生じます。

判断基準は「1文字ごとの変化に即座に反応させる必要があるか」です。

inputを使う場面(リアルタイム性が必要)

  • 1文字入力するごとにインクリメンタルサーチ(検索候補の絞り込み)を行いたい
  • 入力中の文字数をリアルタイムにカウント・制限したい
  • パスワード入力中に「半角英数8文字以上」などの条件達成状況を即時表示したい
const searchInput = document.querySelector('.js-search-input');

if (searchInput) {
  searchInput.addEventListener('input', (event) => {
    // 入力されるたびに候補リストを再計算
    updateSearchSuggestions(event.target.value);
  });
}

changeを使う場面(確定後の処理で十分)

  • 入力が完了したあとに郵便番号から住所を自動補完したい(入力途中で無駄にAPIを呼び出さないため)
  • <select> の選択肢に応じて、次の入力項目の表示・非表示を切り替えたい
  • チェックボックスの状態に応じて「送信」ボタンの活性・非活性を切り替えたい
const termsCheckbox = document.querySelector('.js-terms-check');
const submitBtn = document.querySelector('.js-submit-btn');

if (termsCheckbox && submitBtn) {
  termsCheckbox.addEventListener('change', (event) => {
    // チェックが入っていればボタンのdisabledを解除
    submitBtn.disabled = !event.target.checked;
  });
}

blurイベント|フォーカスが外れたときに発火する

blur イベントは、対象の要素からフォーカスが外れた(要素がフォーカスを失った)瞬間に発火します。

const emailInput = document.querySelector('.js-email');

if (emailInput) {
  emailInput.addEventListener('blur', (event) => {
    console.log('入力欄からフォーカスが外れました:', event.target.value);
  });
}

主な用途は、ユーザーがその入力欄を離れたタイミングで行う入力チェック(単体バリデーション)です。

例えば、「メールアドレスの入力欄から別の欄へ移った瞬間に、@が含まれているか判定してエラーメッセージを表示する」といった処理に適しています。

blurとchangeの違い

テキスト入力欄において、blur と change はどちらも「入力欄から外れたとき」に関係するため混同されがちですが、発火の条件が異なります。

  • blur:値が変わったかどうかに関係なく、通常はフォーカスを失ったときに発火する
  • change:フォーカスが外れたとしても、値に変更がなければ発火しない
操作パターン blur change
入力欄をクリックしてフォーカスを当て、何も入力・変更せずに外をクリックした 発火する 発火しない
文字を入力して内容を変更してから外をクリックした 発火する 発火する

「入力欄に一度触れてから何も書かずに離れた場合(必須項目の未入力チェック)」を検知したい場合は、change ではなく blur を使用します。

注意:blurイベントはバブリングしない

複数の入力項目があるフォームにおいて、親要素(<form> や <div> など)でイベントを1つにまとめて監視したいケースがあります。

しかし、blur イベントは通常のDOMツリー上で親要素に向かってバブリング(浮上)しません。そのため、親要素に対して addEventListener('blur', ...) を設定しても、子要素の <input> で発生した blur は捕捉できません。親要素でまとめてフォーカス外れを検知したい場合は、バブリングする同系統のイベントである focusout を使用するか、キャプチャフェーズ(addEventListener の第3引数に { capture: true })を利用します。

イベントの伝播や親要素での一括監視の仕組みについては、以下の記事で解説しています。

参考:後から追加した要素でclickイベントが動かない原因|イベントデリゲーションの使い方

submitイベント|フォームが送信されるときに発火する

submit イベントは、フォーム全体を送信するトリガーが発生したときに発火します。

<form class="js-form" action="/contact/send" method="post">
  <label for="user-email">メールアドレス</label>
  <input type="email" id="user-email" name="email" required>
  <button type="submit" class="js-submit">送信する</button>
</form>
const formElement = document.querySelector('.js-form');

if (formElement) {
  formElement.addEventListener('submit', (event) => {
    // フォーム送信時の処理
    console.log('フォーム送信が開始されました');
  });
}

フォーム送信そのものを処理したい場合は、送信ボタンの click ではなく、基本的に <form> 要素の submit イベントを監視します。

フォーム送信をclickではなくsubmitで受け取る理由

初心者のコードでよく見られるのが、送信ボタンに対して click イベントを設定してしまうパターンです。

// 注意:送信処理の監視としては不十分になる場合があります
const submitButton = document.querySelector('.js-submit');

if (submitButton) {
  submitButton.addEventListener('click', (event) => {
    // ボタンのクリック操作に依存した書き方
  });
}

送信ボタンの click だけを監視すると、処理が特定のボタン操作に依存します。

フォーム内に既定の送信ボタンがある場合、1行テキスト入力欄でEnterキーを押した際の暗黙送信(implicit submission)でもそのボタンに click イベントが発生することがあります。

しかし、送信ボタンを持たないフォームや、スクリプトからの requestSubmit() 呼び出しなども考慮し、フォームの「送信」という処理そのものを確実に扱う目的では、<form> の submit イベントを監視するのが基本です。

<form> の submit イベントを監視すると、ボタンのクリックやEnterキーなど、フォームが送信段階まで進んだ操作を同じ処理で一元的に扱えます。

// 推奨:form要素のsubmitイベントを監視する
const formElement = document.querySelector('.js-form');

if (formElement) {
  formElement.addEventListener('submit', (event) => {
    // クリックやEnterキーなどによる送信操作を一元的に処理できる
  });
}

preventDefault()は何をしている?

フォームの submit を扱う際、多くのサンプルコードで event.preventDefault() が記述されています。

formElement.addEventListener('submit', (event) => {
  // ブラウザ本来のフォーム送信動作をキャンセルする
  event.preventDefault();

  // JavaScript側で入力チェックやAjax(Fetch API)送信を行う
  console.log('送信を中断してJS側で制御します');
});

ブラウザの通常のフォームは、送信が行われると指定された action 属性のURLへページ遷移を伴うHTTPリクエストを送信します。

これがブラウザの「既定動作(デフォルト動作)」です。

event.preventDefault() を実行すると、このブラウザ本来の送信・ページ遷移動作をキャンセルできます。

どんなときに使うか?

  • 送信前の全体バリデーションでエラーが見つかったとき:入力に不備がある場合、preventDefault() で送信を中断し、画面上にエラー文言を表示します。
  • ページ遷移をせず、非同期(Fetch API / Ajax)でデータを送信したいとき:画面を再読み込みせずにバックエンドと通信する場合に使用します。

JavaScript側で送信処理を制御する必要がある場面で呼び出すのが基本です。

4つのイベントを使った入力・バリデーションの流れ

実際のWebサイトでよくある「リアルタイム文字数カウント」「フォーカスアウト時の単体チェック」「チェックボックス連動」「送信時の最終確認」を組み合わせた実動コード例です。

<form class="js-entry-form">
  <div class="form-group">
    <label for="comment">ご意見・ご要望(10文字以上・50文字以内)</label>
    <textarea id="comment" class="js-comment" rows="4"></textarea>
    <div class="field-info">
      <span class="js-char-counter">0</span> / 50文字
    </div>
    <p class="error-text js-error-message"></p>
  </div>

  <div class="form-group">
    <label>
      <input type="checkbox" class="js-agree-check"> 利用規約に同意する
    </label>
  </div>

  <button type="submit" class="js-submit-button" disabled>送信する</button>
</form>
const form = document.querySelector('.js-entry-form');
const textarea = document.querySelector('.js-comment');
const counter = document.querySelector('.js-char-counter');
const errorMsg = document.querySelector('.js-error-message');
const agreeCheck = document.querySelector('.js-agree-check');
const submitBtn = document.querySelector('.js-submit-button');

// 1. inputイベント:文字数のリアルタイム表示
if (textarea && counter) {
  textarea.addEventListener('input', (event) => {
    counter.textContent = event.target.value.length;
  });
}

// 2. blurイベント:入力欄を離れたタイミングでの単体チェック
if (textarea && errorMsg) {
  textarea.addEventListener('blur', (event) => {
    const value = event.target.value.trim();
    if (value.length < 10) {
      errorMsg.textContent = '10文字以上で入力してください。';
    } else if (value.length > 50) {
      errorMsg.textContent = '50文字以内で入力してください。';
    } else {
      errorMsg.textContent = '';
    }
  });
}

// 3. changeイベント:同意チェックボックスの切り替えでボタン活性化
if (agreeCheck && submitBtn) {
  agreeCheck.addEventListener('change', (event) => {
    submitBtn.disabled = !event.target.checked;
  });
}

// 4. submitイベント:送信時の全体バリデーション
if (form && textarea && agreeCheck && errorMsg) {
  form.addEventListener('submit', (event) => {
    const value = textarea.value.trim();

    // 文字数チェック
    if (value.length < 10 || value.length > 50) {
      event.preventDefault();
      errorMsg.textContent = '10文字以上50文字以内で入力してください。';
      textarea.focus();
      return;
    }

    // 利用規約チェック
    if (!agreeCheck.checked) {
      event.preventDefault();
      errorMsg.textContent = '利用規約への同意が必要です。';
      agreeCheck.focus();
      return;
    }

    // すべて通過した場合(デモ用として送信をキャンセルしてログ出力)
    event.preventDefault();
    errorMsg.textContent = '';
    console.log('バリデーション通過: データを送信します');
  });
}

input・change・blur・submitがどのタイミングで発火するのかは、下のCodePenでも確認できます。フォームを実際に操作しながら、イベントログの変化を見比べてみてください。

See the Pen Web制作JS|フォームイベント|input・change・blur・submit比較サンプル by WebToolGen (@webtoolgen) on CodePen.

どのイベントを使えばよい?用途別まとめ

フォーム実装時に迷った際は、以下の対応表を目安にイベントを選定してください。

実装したい要件 推奨イベント 理由
入力文字数をリアルタイムにカウントする input キー入力・削除・ペーストに即座に追従させるため
インクリメンタルサーチ(候補のリアルタイム表示) input 入力途中の文字列に合わせて検索を更新するため
<select> の選択変更で連動UIを切り替える change 選択が確定した瞬間に発火させるため
チェックボックスON/OFFでボタンの活性を切り替える change チェック状態が切り替わった瞬間に検知するため
入力欄から離れた時点で書式(メール・電話等)をチェック blur 入力中の誤判定を防ぎ、離れた時点で確認するため
必須項目に触れてそのまま離れた場合の空欄チェック blur 値に変更がなくてもフォーカス外れを検知できるため
フォーム送信直前の全体バリデーション・送信制御 submit ボタンクリックやEnterキー送信などの送信操作を一元管理できるため

フォームイベントが動かないときの確認手順

フォーム関連のイベントリスナーが期待通りに動作しない場合は、ブラウザの開発者ツールを開き、以下のステップ順に状態を確認してください。

ステップ1:対象の要素がDOM上に取得できているか(nullの確認)

まず、イベントを登録する対象要素が正常に取得できているか確認します。

const form = document.querySelector('.js-entry-form');
console.log(form);

コンソールに null が出力されている場合、セレクタ文字列のスペルミスか、HTMLの読み込み(DOMツリー構築)が完了する前にスクリプトが実行されています。

参考:querySelectorで要素を取得できない・nullになる原因と確認方法

ステップ2:inputとchangeの発火タイミングを混同していないか

「文字を入力しているのにイベントが動かない」という場合、change を設定していないか確認してください。

前述の通り、テキスト入力欄の change はフォーカスを外すまで発火しません。

リアルタイムに検知したい場合は input に変更します。

ステップ3:submitイベントをbutton要素に登録していないか

Elementsパネルで対象の要素構造を確認してください。

<button type="submit"> に対して addEventListener('submit', ...) を設定しても発火しません。

submit イベントは親要素の <form> に対して登録する必要があります。

クリック時に処理が動かない一般的なトラブルについては、以下の記事も確認してください。

参考:addEventListenerでclickイベントが動かない原因と確認方法

ステップ4:HTMLの標準バリデーションで送信が止まっていないか

<input required>、type="email"、pattern などの制約検証属性を指定している場合、入力値が不正な状態だとブラウザの標準バリデーション(制約検証)によって送信が中断され、submit イベントまで到達しないことがあります(ブラウザ内部では invalid イベントが発生します)。form.addEventListener('submit', ...) が動かない場合は、入力欄がHTML側の制約を満たしているかも確認してください。

ステップ5:blurイベントを親要素でリッスンしようとしていないか

親要素に対してイベントデリゲーションを行おうとして addEventListener('blur', ...) を設定している場合、blur はバブリングしないため検知できません。

親要素でまとめて監視したい場合は focusout を使用してください。

参考:後から追加した要素でclickイベントが動かない原因|イベントデリゲーションの使い方

ステップ6:JavaScriptからvalueを書き換えていないか

スクリプト側で element.value = '...' のように値を更新しても、input や change イベントは自動的には発火しません。

プログラムからの値変更に連動させたい場合は、処理関数を明示的に呼び出す設計になっているか確認してください。

まとめ

フォームで扱う4つのイベントは、役割と発火条件を正しく把握することで迷わず使い分けられます。

  • 入力中の即時反映には input
  • 選択や入力の確定には change
  • 入力欄から離れた瞬間の検証には blur
  • フォーム全体の送信と最終確認には submit

それぞれの特徴に合わせて適切に使い分けることで、ユーザーの操作感を損なわず、堅牢なフォーム制御を実装できます。