CSS講座 / フォームの状態:フォーカス・検証・disabled・readonly
CSSはフォームのフォーカスや入力の妥当性などを見た目で示せますが、項目名、制約、送信の振る舞いはHTMLが定義します。:invalidは操作前にも一致し、:user-invalidは操作後にブラウザーが適切と判断する時点でエラー表示へ使えます。
ラベルと補足説明を結び付ける
labelのforと入力のidを一致させると項目名が関連付きます。placeholderは入力後に消えるため、項目名や重要な条件の代わりにはしません。補足は見える文章として残し、そのidをaria-describedbyから参照します。
<label for="email">Email address (required)</label> <input id="email" name="email" type="email" required placeholder="name@example.com" aria-describedby="email-help"> <p id="email-help">Enter name@example.com in the same format.</p>
これは完全例のメール部分を短く示したコードです。idは文書内の識別、nameは送信データの項目名です。画面で入力できてもnameがなければ通常の送信データには入りません。W3Cのラベル、入力案内を参照してください。
狭い画面と文字拡大に収まる入力欄
フォームに最大幅を設け、入力欄にはwidth: 100%とborder-boxを組み合わせます。枠とpaddingを幅へ含めないと、親の100%にさらに余白が加わります。
*, *::before, *::after { box-sizing: border-box; }
form { max-width: 28rem; }
input { width: 100%; min-width: 0; padding: .65rem; font: inherit; }
この指定は完全例にも含まれます。font: inheritで本文の書体・サイズ・行高を引き継ぎ、min-widthでFlex・Grid内の縮小を妨げる最小幅にも対応します。固定高さを避け、説明や文字が増えたときに後ろの項目も自然に下がる構成にします。
フォーカスと入力エラーを分けて表示する
:focusは現在フォーカスを持つ要素、:focus-visibleはブラウザーが表示すべきと判断したフォーカスへ一致します。文字入力欄などはマウス操作でも一致する場合があるため、「キーボード専用」とは限りません。MDNのfocus-visibleを参照してください。
現在操作している位置と、入力条件を満たさないエラーは同時に存在します。完全例では青いoutlineと赤いborder・文章を分けています。outlineを消したまま代替を設けたり、親の切り取りで枠を隠したりしないでください。
:invalidは空の必須欄にも最初から一致します。:user-invalidの具体的な時点はブラウザーの操作判定にも依存するため、実習では送信を試して確認します。未対応なら装飾が出なくてもrequiredなどのHTML検証が消えるわけではありません。必要な対応環境で表示の流れを設計します。MDNのuser-invalid、W3Cの非テキストのコントラストを参照してください。
required・disabled・readonlyの違い
requiredは入力を求め、valid・invalidは制約に合うかを表します。disabledは通常Tab順と送信データから除かれます。対応するテキスト系入力のreadonlyは編集を禁止しますが、フォーカスや選択が可能で、nameがあれば送信へ含められます。
readonlyはselectを読み取り専用にする一般的な属性ではありません。対応する読み取り専用の入力は制約検証の対象から外れる点も、disabledとの違いです。HTML仕様の送信データ構築、MDNのreadonlyを参照してください。
メールの例はname@example.comの形式を確認するもので、アカウントの存在や受信可能性は保証しません。HTMLのemail検証はあらゆる国際化アドレスを受け付けるものでもなく、非ASCIIのローカル部が必ず通るとは扱いません。HTML仕様のemail入力で条件を確認します。
完全例で状態と送信項目を調べる
次をform-states.htmlとして保存します。最後の短いJavaScriptは妥当な送信を止めて項目一覧を表示する補助で、サーバーへ送ったり追加検証したりしません。JavaScriptを無効にすると結果表示は動きません。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Comparing form states and submitted fields</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; padding: 1rem; color: #183044; font-family: sans-serif; line-height: 1.6; }
main { max-width: 46rem; margin-inline: auto; }
h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
button, input { font: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
form { max-width: 28rem; }
.field { margin-block: 1.25rem; }
label { display: block; margin-bottom: .35rem; font-weight: 700; }
input { display: block; width: 100%; min-width: 0; padding: .65rem;
border: 2px solid #6d7e8b; border-radius: .3rem; background: white; color: #183044; }
input:required { border-inline-start-width: 4px; }
input:user-invalid { border-color: #9d2632; }
.help, .error { margin-block: .4rem 0; }
.error { display: none; color: #9d2632; }
input:user-invalid + .error { display: block; }
input[readonly] { background: #eef6fa; }
input:disabled { background: #eceff2; color: #465766; cursor: not-allowed; }
button { padding: .65rem 1rem; border: 1px solid #164f73; border-radius: .3rem;
background: #166b92; color: white; }
output { display: block; margin-block: 1rem; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<main>
<h1>Check input states and submitted data</h1>
<form id="demo-form">
<div class="field">
<label for="email">Email address (required)</label>
<input id="email" name="email" type="email" autocomplete="email" required
placeholder="name@example.com" aria-describedby="email-help email-error">
<p id="email-error" class="error">Enter an email address in a format such as name@example.com; do not leave the field empty.</p>
<p id="email-help" class="help">This sample input checks the address format. It does not check whether the example address receives mail.</p>
</div>
<div class="field">
<label for="plan">Plan (read-only)</label>
<input id="plan" name="plan" type="text" value="Basic" readonly>
</div>
<div class="field">
<label for="coupon">Coupon (currently disabled)</label>
<input id="coupon" name="coupon" type="text" value="WELCOME" disabled>
</div>
<button type="submit">Check submitted fields</button>
</form>
<output id="result" for="email plan coupon" aria-live="polite"></output>
<script>
const form = document.getElementById("demo-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const entries = Array.from(new FormData(form).entries());
document.getElementById("result").textContent = JSON.stringify(entries, null, 2);
});
</script>
</main>
</body>
</html>
- 空欄のまま送信を試し、必須入力の案内とエラー文を確認します。
- abcを入れて試し、形式エラーでは新しい結果一覧が出ないかを見ます。
- name@example.comで試します。emailとplanが含まれ、disabledのcouponは含まれません。
- Tabでメール、プラン、送信へ進み、couponを飛ばしてreadonlyのplanには入れるか確認します。
- couponのdisabledだけを外し、フォーカスと送信への追加を見て戻します。
- planのreadonlyを外して編集し、値をBasicへ戻してreadonlyも復元します。
- 画面を狭めたり文字を拡大したりし、入力、フォーカス枠、エラー文に切れや重なりがないか確認します。
CSSとブラウザー検証の範囲を理解する
CSSは状態を示す役割です。ブラウザー検証は入力ミスを減らしますが、開発者ツールや別のリクエストで回避できるため、実サービスでは許される値、長さ、権限などをサーバーで再検証します。サーバーエラーでは入力を保つ流れも必要です。
動作が違うならforとid、name、required、type、disabled、readonlyを先に見ます。フォーカスが消えるならoutlineの上書き、最初から赤いならinvalid、送信後も文が出ないならuser-invalidの対応と隣接セレクターを調べます。結合子と疑似クラス、リンクのフォーカスも参照してください。




