非表示にした項目のrequiredでフォームが送信できない原因と直し方

症状:非表示にした項目のせいでフォームが送信できない

回答に応じて入力欄を出し分ける条件分岐フォームで、必要ないときに required 付きの項目を display:nonehidden で隠したとします。すると、その項目が関係ない選択をしているのに送信ボタンが効かない。画面には何のメッセージも出ないのに、送信だけがブロックされる。開発者ツールのConsoleを開くと、次の英語のエラーが出ています。

An invalid form control with name='…' is not focusable.

これは「バリデーションに引っかかっている必須項目があるのに、その項目が画面に見えていない(フォーカスできない)ので吹き出しを出せない」という状態です。ブラウザは送信を止めますが、どこが悪いのかを画面上に示せません。逆に、非表示にした項目の入力値がそのまま送信データに残るという反対向きの事故も同じ設計ミスから起きます。この記事では両方を、失敗する様子と直したあとの様子を動くデモで比べながら解説します。コピペで動く完成形は条件分岐フォームの事例ページに置いてあります。

送信できない主な原因

「an invalid form control is not focusable」で送信が止まるとき、原因はほぼ次の3つのどれかです。上から順に確認してください。

原因1:required を外さず display:none / hidden で隠している

最も多いパターン。項目を非表示にしただけで required を付けたままにしているため、未入力の必須項目としてバリデーション対象に残り、送信が止まる。

原因2:親ブロックごと隠して、中の required 子要素を見落としている

住所欄をまとめた div を丸ごと非表示にしたが、その中の input 個々の required は生きている。親を隠したので気づきにくい。

原因3:setCustomValidity のエラーを解除し忘れている

独自バリデーションで setCustomValidity('メッセージ') を設定した項目を、条件が変わって不要になっても setCustomValidity('') で解除していない。非表示にすると同じ「見えない不正な項目」になる。

いずれも本質は同じで、画面から見えなくなった項目が、バリデーション上はまだ「未入力の必須項目」として残っていることです。以下、原因1を失敗デモで再現してから直します。

失敗例:非表示なのに required が残っている

下のデモは原因1をわざと再現したものです。「その他」を選んだときだけ自由記入欄(required 付き)を表示する条件分岐フォームですが、非表示にするときに required を外していません。「資料請求」か「見積依頼」を選んでから送信してみてください。自由記入欄は関係ないはずなのに送信できません。ブラウザのConsoleも開いて確認すると分かりやすいです。

壊れた実装(非表示にするだけで required が残る)

「資料請求」または「見積依頼」を選んで送信すると、見えない自由記入欄の required が原因で送信がブロックされます。

お問い合わせ種別を選択してください

// NG: 非表示にするとき hidden を付けるだけ。required も value もそのまま
radios.forEach(function (r) {
  r.addEventListener('change', function () {
    if (r.value === 'other') {
      detail.hidden = false;       // 表示するだけ
    } else {
      detail.hidden = true;        // 隠すだけ。required が残ったまま → 送信できない
    }
  });
});

「その他」を選んで自由記入欄を空のまま送信した場合は、項目が表示されているので普通の吹き出し(このフィールドを入力してください)が出ます。問題は「資料請求」など自由記入欄と関係ない選択をしたときにも送信できないことです。ここを直します。

原因の見分け方

Consoleの name='…' の部分に、引っかかっている項目の name 属性が入ります。まずここで犯人の項目を特定してください。name が空なら name 属性を付けていない項目です。

原因2の「親ブロックごと隠した」ケースは特に気づきにくいです。住所欄をまとめた div を非表示にしても、中の input 一つひとつの required は生きています。チェックボックスで住所ブロックを出し入れするような実装では、ブロックを隠すときに中の各項目の required も外す必要があります。この「ブロック単位の出し分け」を含む正しい実装は条件分岐フォームの事例ページのPattern 2(請求先住所の表示切替)で動かせます。

原因3は、独自の入力チェックで setCustomValidity() を使っている場合だけ該当します。エラー文字列をセットしたら、条件が解消したタイミングで必ず el.setCustomValidity('') を呼んで空に戻します。空文字を渡すことで「もう不正ではない」とブラウザに伝わります。

直し方:非表示にするときは値クリア・disabled化・required着脱をセットにする

直し方はシンプルです。項目を非表示にするときに、表示を消すだけでなく入力値のクリアと disabled 化、required の解除をまとめて行うことです。逆に表示するときは disabled を外して required を付け直します。下のデモは同じフォームをこの方針で直したものです。「資料請求」を選んで送信すると、今度はきちんと送信できます。

直した実装(非表示時に値クリア・disabled化・required解除)

「資料請求」「見積依頼」なら送信でき、「その他」で空のときだけ表示中の項目に吹き出しが出ます。

お問い合わせ種別を選択してください

function showDetail() {
  detail.hidden = false;
  detail.disabled = false;
  detail.setAttribute('required', '');   // 表示するので必須に戻す
}

function hideDetail() {
  detail.hidden = true;
  detail.value = '';                     // 古い入力を残さない
  detail.disabled = true;                // 送信データから外す
  detail.removeAttribute('required');    // 見えない必須項目にしない
}

radios.forEach(function (r) {
  r.addEventListener('change', function () {
    r.value === 'other' ? showDetail() : hideDetail();
  });
});

ポイントは、disabled にした項目はバリデーションの対象からも外れることです。つまり disabled を付ければ、厳密には required を明示的に外さなくても送信は止まりません。それでも上のコードで required も外しているのは、あとで disabled を使わない実装に変えたときの事故を防ぐためと、状態を読んで意図が分かるようにするためです。

どれを使うか:送信対象に含めるかで決める

「値クリア」「disabled化」「required着脱」の3つは、非表示にした項目を送信データに含めたいかどうかで使い分けが決まります。迷ったら次の表で判断してください。

非表示にした項目をどうしたいか 値クリア disabled化 required の明示着脱
送信対象から外す(条件分岐フォームの定番) する する 不要(disabledでバリデーションからも自動で外れる)
表示は消すが値はサーバへ送りたい しない 使わない(disabledにすると送信されない) 必要(未入力を許すなら required を外す)
入力を一時的に禁止したいだけ(グレーアウト表示は残す) 任意 する 不要

大半の条件分岐フォームは一番上の行に当てはまります。disabled 化と値クリアをセットにしておけば、送信できない事故(required残り)と、次に説明する値が残る事故の両方を同時に防げます。

逆の罠:非表示項目の値がそのまま送信される

もう一方の事故は、非表示にしたのに古い入力値が送信データに残ることです。hiddendisplay:none で見えなくしても、disabled を付けていない項目は FormData にも実際の送信にも含まれます。ユーザーがいったん入力してから条件を変えて隠した値が、そのままサーバに送られてしまいます。

下のデモで確かめてください。「請求先メモ」には古い値が入っています。非表示にしても、disabled を付けない限り送信データに残ることが分かります。

送信データに何が入るかを確認する

「送信データを確認」を押すと、この内容でサーバに送られる項目を表示します。

非表示にしただけ(disabledなし)だと memo が送信データに残り、disabled を付けると消えるのが確認できます。前の章の直し方で値クリアと disabled 化をセットにしていたのは、この事故も同時に防ぐためです。

// hidden や display:none だけでは送信データに残る
var data = new FormData(form);
data.get('memo'); // → '以前入力した古い値'(隠しても取得できてしまう)

// disabled を付けた項目は FormData からも送信からも外れる
memo.disabled = true;
var data2 = new FormData(form);
data2.get('memo'); // → null

まとめ

条件分岐で項目を出し分けるときは、表示・非表示だけを切り替えるのではなく、非表示にする側で必ず3点セットを行うのが基本です。

値をクリアする → disabled を付けて送信対象から外す → required を外して見えない必須項目にしない。

この3つをまとめて行えば、「an invalid form control is not focusable」で送信できない事故と、隠したはずの値が送信される事故の両方を防げます。表示に戻すときは disabled を外して required を付け直すだけです。動くコピペ用の完成形は下の関連事例で確認してください。連動プルダウンなど別のフォーム連動でつまずいている場合は連動プルダウンで前の選択値が残る・リセットされない原因と直し方も合わせてどうぞ。

関連するUI事例

この記事の修正コードに対応する、コピペで動くフォームの事例です。ラジオボタン連動とチェックボックスによるブロック単位の表示切替を事例ページで確認できます。