- CSS
CSSフォームデザイン|input・select・チェックボックス装飾
CSSのフォームデザインは、「リセット → 共通の見た目 → 状態(hover・focus・エラー・disabled)」の順に重ねると、少ないコードで全部品をそろえられます。 この記事では、input・textarea・select・checkbox・radio・ボタンの6部品を、HTML と CSS のセットで、状態ごとの見た目までそろえたコピペ集としてまとめました。
土台のリセットは :where() で詳細度を 0 にしてあるので、!important を1つも使わずに上書きできます。チェックボックスは accent-color の1行で済ませる版と、appearance: none で作り込む版を並べ、display: none で隠すとキーボードで操作できなくなる例も実際に動かして確かめました。:focus-visible、:user-invalid、field-sizing、ダークモード、コントラスト比は、Chromium 141 で取り出した値を載せています。
CSSフォームデザインの早見表
部品 × 状態のセレクタ
| 部品 | 通常 | hover | focus | エラー | disabled |
|---|---|---|---|---|---|
| input・textarea | .f-input |
.f-input:hover |
.f-input:focus-visible |
.f-input:user-invalid |
.f-input:disabled |
| select | .f-select |
.f-select:hover |
.f-select:focus-visible |
.f-select:user-invalid |
.f-select:disabled |
| checkbox | .f-check |
.f-check:hover |
.f-check:focus-visible |
.f-check:user-invalid |
.f-check:disabled |
| radio | .f-radio |
.f-radio:hover |
.f-radio:focus-visible |
(グループ単位で表示) | .f-radio:disabled |
| ボタン | .f-btn |
.f-btn:hover |
.f-btn:focus-visible |
- | .f-btn:disabled |
少ないCSSで済む方法
| やりたいこと | 書き方 | 対応ブラウザ(最初の版) |
|---|---|---|
| チェックボックス・ラジオの色を変える | accent-color: #1d4ed8; |
Chrome 93 / Firefox 92 / Safari 15.4 |
| textarea を入力量に合わせて伸ばす | field-sizing: content; |
Chrome 123 / Firefox 152 / Safari 26.2 |
| キーボード操作のときだけフォーカス枠を出す | :focus-visible |
Chrome 86 / Firefox 85 / Safari 15.4 |
| 入力し終えてからエラー表示にする | :user-invalid |
Chrome 119 / Firefox 88 / Safari 16.5 |
| ライト・ダークの色を1行で書く | light-dark(#fff, #111827) |
Chrome 123 / Firefox 120 / Safari 17.5 |
| iOS で入力時に拡大されないようにする | font-size: max(16px, 1rem); |
- |
| ブラウザ標準の見た目を外す | appearance: none; |
Chrome 84 / Firefox 80 / Safari 15.4 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSフォームデザイン|input・select・チェックボックス装飾」
検証した環境とファイル構成
| 項目 | 内容 |
|---|---|
| ブラウザ | Chromium 141.0.7390.37(Playwright 1.56.0 から起動。画面幅 560px) |
| 対応ブラウザの版 | npm の @mdn/browser-compat-data 8.1.4(2026-10-01 付)から取得 |
| 確認方法 | getComputedStyle() の値、matches() の結果、Tab キー・Space キーの操作、スクリーンショットの目視 |
Firefox・Safari・iOS の実機での確認は行っていません(未実行)。該当する箇所はその都度明記します。
CSS は1つのファイル(form.css)で、この記事に出てくる CSS のコードブロックを上から順につなげると完成形になります。クラス名はすべて f- で始めてあり、既存のスタイルとぶつかりにくくしています。
土台:色の変数と!importantなしのリセット
色と基本値
/* ===== 1. 色と基本値(ライト/ダーク) ===== */
:root {
color-scheme: light dark;
--f-bg: light-dark(#ffffff, #111827);
--f-text: light-dark(#1f2937, #f3f4f6);
--f-muted: light-dark(#4b5563, #d1d5db);
--f-border: light-dark(#6b7280, #9ca3af);
--f-accent: light-dark(#1d4ed8, #93c5fd);
--f-on-accent: light-dark(#ffffff, #111827);
--f-error: light-dark(#b91c1c, #fca5a5);
--f-disabled-bg: light-dark(#e5e7eb, #374151);
--f-disabled-text: light-dark(#4b5563, #d1d5db);
--f-radius: 8px;
}
color-scheme: light dark は、ライトとダークの両方に対応していることをブラウザに伝える宣言です。light-dark(ライトの色, ダークの色) は、そのときの配色に合う側を自動で選びます。色を変数にまとめておくと、ブランドの色に合わせるときは --f-accent の1行を変えるだけで済みます。
リセット
/* ===== 2. リセット(:where で詳細度 0) ===== */
:where(input, textarea, select, button) {
box-sizing: border-box;
margin: 0;
font: inherit;
color: inherit;
letter-spacing: inherit;
}
:where(textarea) { resize: vertical; }
:where(button, select, label, input[type="checkbox"], input[type="radio"]) { cursor: pointer; }
:where(:disabled, :disabled + label) { cursor: not-allowed; }
フォーム部品は、初期状態では周りの文字の設定を引き継ぎません。body に font: 18px/1.6 serif と文字色を指定したページで、リセットの前後を取り出した結果です。
リセット前 input: 13.3333px Arial rgb(0, 0, 0) | textarea: 13.3333px monospace rgb(0, 0, 0) | select: 13.3333px Arial rgb(0, 0, 0) | button: 13.3333px Arial rgb(0, 0, 0)
リセット後 input: 18px serif rgb(10, 20, 30) | textarea: 18px serif rgb(10, 20, 30) | select: 18px serif rgb(10, 20, 30) | button: 18px serif rgb(10, 20, 30)
(各行の先頭のラベルは、読みやすいよう要素名に置き換えています。)リセット前は文字サイズが 13.3333px で、textarea だけ等幅フォントになっています。font: inherit と color: inherit の2行で、4つの部品がページの文字と同じ設定にそろいました。
:where() で包んであるのは、詳細度を 0 にするためです。リセットの後に input { color: rgb(200,0,0); } という要素セレクタ1つのルールを足したところ、input だけが rgb(200, 0, 0) に変わりました。
| リセットの書き方 | 詳細度 | 後から上書きするには |
|---|---|---|
:where(input, textarea, select, button) { } |
0,0,0 | 要素セレクタやクラス1つで足りる |
input, textarea, select, button { } |
0,0,1 | クラス1つで足りる |
.form input { } |
0,1,1 | 同じかそれ以上の詳細度が必要 |
input { ... !important } |
- | !important どうしの競争になる |
上書きが効かないときに !important を足すのではなく、土台の詳細度を下げておくのが、後から手を入れやすい作り方です。
項目の枠(ラベル・補足・エラー文)
<div class="f-field">
<label class="f-label" for="email">メールアドレス</label>
<input class="f-input" id="email" name="email" type="email" required autocomplete="email"
aria-describedby="email-help email-error">
<span class="f-help" id="email-help">返信先として使います。</span>
<span class="f-error" id="email-error">メールアドレスの形式で入力してください。</span>
</div>
/* ===== 3. 項目の枠(ラベル・補足・エラー文) ===== */
.f-field { display: grid; gap: 6px; margin-block-end: 20px; }
.f-label { font-weight: 700; color: var(--f-text); }
.f-help { font-size: 0.875rem; color: var(--f-muted); }
.f-error { display: none; font-size: 0.875rem; color: var(--f-error); }
.f-field:has(:user-invalid, [aria-invalid="true"]) .f-error { display: block; }
fieldset.f-field { margin-inline: 0; padding: 0; border: 0; min-inline-size: 0; }
legend.f-label { padding: 0; }
| 要素 | 役割 | ポイント |
|---|---|---|
label の for |
入力欄と結び付ける | ラベルをクリックしても入力欄にフォーカスが移る。読み上げにも使われる |
aria-describedby |
補足とエラー文を入力欄に結び付ける | 入力欄にフォーカスしたとき、補足も読み上げの対象になる |
.f-error |
エラー文 | 初期は非表示。項目の中に :user-invalid の部品があるときだけ :has() で表示 |
autocomplete |
自動入力の種類 | 入力の手間を減らせる |
placeholder はラベルの代わりにはなりません。入力を始めると消えてしまうためです。ラベルは常に見える位置に置き、placeholder は入力例を添える用途に使います。
input・textarea・selectのデザイン
inputの HTML と CSS
<div class="f-field">
<label class="f-label" for="name">お名前</label>
<input class="f-input" id="name" name="name" type="text" required autocomplete="name"
placeholder="例:山田 太郎" aria-describedby="name-error">
<span class="f-error" id="name-error">お名前を入力してください。</span>
</div>
<div class="f-field">
<label class="f-label" for="code">会員番号(変更できません)</label>
<input class="f-input" id="code" name="code" type="text" value="M-00123" disabled>
</div>
/* ===== 4. input・textarea・select 共通 ===== */
.f-input, .f-select {
inline-size: 100%;
min-block-size: 44px;
padding: 8px 12px;
font-size: max(16px, 1rem);
line-height: 1.5;
color: var(--f-text);
background-color: var(--f-bg);
border: 1px solid var(--f-border);
border-radius: var(--f-radius);
transition: border-color 0.15s, box-shadow 0.15s;
}
.f-input::placeholder { color: var(--f-muted); opacity: 1; }
.f-input:hover, .f-select:hover { border-color: var(--f-text); }
.f-input:focus-visible, .f-select:focus-visible {
outline: 2px solid var(--f-accent);
outline-offset: 2px;
border-color: var(--f-accent);
}
.f-input:user-invalid, .f-select:user-invalid,
.f-input[aria-invalid="true"], .f-select[aria-invalid="true"] {
border-color: var(--f-error);
box-shadow: 0 0 0 1px var(--f-error);
}
.f-input:disabled, .f-select:disabled {
color: var(--f-disabled-text);
background-color: var(--f-disabled-bg);
border-style: dashed;
}
.f-input:read-only:not(select) { background-color: var(--f-disabled-bg); }
状態ごとに取り出した値です(ライトモード)。
| 状態 | 枠線 | 背景 | そのほか |
|---|---|---|---|
| 通常 | rgb(107, 114, 128) の実線 | rgb(255, 255, 255) | 高さ 44px、文字 16px |
| hover | rgb(31, 41, 55)(文字色と同じ濃さ) | 変化なし | - |
| focus | アクセント色 | 変化なし | 外側に 2px のアウトライン |
| エラー | rgb(185, 28, 28) | 変化なし | 内側に 1px の影を足して太く見せる |
| disabled | 破線 | rgb(229, 231, 235) | 文字 rgb(75, 85, 99)、カーソル not-allowed |
readonly を付けた入力欄は、枠は実線のまま、背景だけが disabled と同じ rgb(229, 231, 235) になります。
エラーと disabled は、色だけでなく形(太さ・破線)でも違いがわかるようにしてあります。色の見分けが付きにくい環境でも状態が伝わります。高さの 44px は、指で押しやすい大きさの目安として使われる値です。
textarea:入力量に合わせて伸ばす
<div class="f-field">
<label class="f-label" for="body">内容</label>
<textarea class="f-input" id="body" name="body" required aria-describedby="body-error"></textarea>
<span class="f-error" id="body-error">内容を入力してください。</span>
</div>
/* textarea:入力量に合わせて伸びる */
textarea.f-input {
min-block-size: 5lh;
max-block-size: 14lh;
field-sizing: content;
}
field-sizing: content は、入力欄の大きさを中身に合わせる指定です。lh は「1行分の高さ」を表す単位で、5lh は5行分になります。行の高さ 24px の textarea で、行数を変えて高さを測りました。
| 入力した行数 | 高さ | 説明 |
|---|---|---|
| 0行(空) | 120px | min-block-size: 5lh(24px × 5) |
| 8行 | 210px | 中身に合わせて伸びた(24px × 8 + 上下の余白と枠 18px) |
| 30行 | 336px | max-block-size: 14lh(24px × 14)で止まり、中はスクロール |
JavaScript なしで、入力量に応じて伸びる textarea になります。field-sizing に対応していないブラウザでは、5行分の高さのまま表示され、右下のつまみで縦に伸ばせます。
select:矢印を作り直す
<div class="f-field">
<label class="f-label" for="topic">お問い合わせの種類</label>
<select class="f-select" id="topic" name="topic" required aria-describedby="topic-error">
<option value="">選択してください</option>
<option value="order">注文について</option>
<option value="member">会員登録について</option>
<option value="other">その他</option>
</select>
<span class="f-error" id="topic-error">種類を選んでください。</span>
</div>
/* select:矢印を currentColor で描く */
.f-select {
appearance: none;
padding-inline-end: 40px;
background-image:
linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 20px center, right 14px center;
background-size: 6px 6px;
background-repeat: no-repeat;
}
appearance: none でブラウザ標準の矢印を外し、2つの linear-gradient を組み合わせて下向きの三角を描いています。矢印の色に currentColor(文字色)を使っているので、ダークモードや disabled でも文字と同じ色に自動で変わります。 画像ファイルや SVG を用意する必要もありません。
先頭の「選択してください」は value="" にしてあります。required と組み合わせると、未選択のまま送信しようとしたときにエラー状態になります。開いたときの選択肢リストまでデザインできる新しい仕組み(appearance: base-select)は Chrome 135 以降など一部のブラウザから使えるようになっていますが、この記事では扱いません(未実行)。
チェックボックス・ラジオボタンのカスタム
方法は2つあります。色を変えるだけなら A、形や大きさまで決めたいなら B を選びます。
| A:accent-color | B:appearance: none | |
|---|---|---|
| CSS の量 | 1行 | 20行ほど |
| 変えられるもの | チェック時の色 | 形・大きさ・枠・チェックマーク・動き |
| キーボード操作 | そのまま使える | そのまま使える(input 自体を残すため) |
| チェックマークの色 | ブラウザが見やすい色を選ぶ | 自分で指定する |
| 向いている場面 | 手早くブランドの色に合わせたい | ほかの部品とデザインを細かくそろえたい |
A:accent-colorの1行版
<label class="f-choice">
<input class="f-accent" id="news" type="checkbox" name="news"> お知らせを受け取る
</label>
/* ===== 5. checkbox・radio(A: accent-color の1行版) ===== */
.f-accent { accent-color: var(--f-accent); inline-size: 1.25em; block-size: 1.25em; }
取り出した値は accentColor: rgb(29, 78, 216) / appearance: auto / width: 20px でした。ブラウザ標準の部品のまま、チェック時の色だけが変わります。大きさは inline-size と block-size で指定できます。accent-color は radio、range(スライダー)、progress にも効きます。
B:appearance:noneで作り込む版
<fieldset class="f-field">
<legend class="f-label">連絡方法</legend>
<label class="f-choice"><input class="f-radio" type="radio" name="contact" value="mail" checked> メール</label>
<label class="f-choice"><input class="f-radio" type="radio" name="contact" value="tel"> 電話</label>
<label class="f-choice"><input class="f-radio" type="radio" name="contact" value="post" disabled> 郵送(準備中)</label>
</fieldset>
<div class="f-field">
<label class="f-choice">
<input class="f-check" id="agree" type="checkbox" name="agree" required aria-describedby="agree-error">
利用規約に同意する
</label>
<span class="f-error" id="agree-error">同意にチェックを入れてください。</span>
</div>
/* ===== 6. checkbox・radio(B: appearance:none の作り込み版) ===== */
.f-choice { display: flex; align-items: center; gap: 8px; min-block-size: 44px; color: var(--f-text); }
.f-check, .f-radio {
appearance: none;
flex: none;
display: inline-grid;
place-content: center;
inline-size: 1.375em;
block-size: 1.375em;
background-color: var(--f-bg);
border: 2px solid var(--f-border);
transition: background-color 0.15s, border-color 0.15s;
}
.f-check { border-radius: 4px; }
.f-radio { border-radius: 50%; }
.f-check::before, .f-radio::before {
content: "";
inline-size: 0.75em;
block-size: 0.75em;
background-color: var(--f-on-accent);
transform: scale(0);
transition: transform 0.12s;
}
.f-check::before { clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%); }
.f-radio::before { border-radius: 50%; inline-size: 0.5em; block-size: 0.5em; }
.f-check:hover, .f-radio:hover { border-color: var(--f-text); }
.f-check:checked, .f-radio:checked { background-color: var(--f-accent); border-color: var(--f-accent); }
.f-check:checked::before, .f-radio:checked::before { transform: scale(1); }
.f-check:focus-visible, .f-radio:focus-visible { outline: 2px solid var(--f-accent); outline-offset: 2px; }
.f-check:user-invalid, .f-check[aria-invalid="true"] { border-color: var(--f-error); }
.f-check:disabled, .f-radio:disabled { background-color: var(--f-disabled-bg); border-style: dashed; }
.f-check:disabled:checked::before, .f-radio:disabled:checked::before { background-color: var(--f-disabled-text); }
.f-choice:has(:disabled) { color: var(--f-disabled-text); }
仕組みは次のとおりです。
appearance: noneで標準の見た目を外し、input 要素そのものに枠と背景を付ける::beforeでチェックマーク(clip-pathで切り抜いた形)や丸を用意し、transform: scale(0)で隠しておく:checkedのときに背景をアクセント色にし、::beforeをscale(1)にして表示する
input を消さずにそのまま装飾しているので、Tab キーでの移動も Space キーでの切り替えも、何も足さずに動きます。 実際に #agree にフォーカスして Space キーを押したところ、checked: true になりました。ラジオボタンをまとめる fieldset と legend は、「連絡方法」というグループ名を読み上げに伝える役割を持ちます。
| 状態 | checkbox / radio の見た目 |
|---|---|
| 通常 | 2px の枠、背景はページと同じ |
| hover | 枠が文字色と同じ濃さになる |
| checked | 背景と枠がアクセント色、チェックマーク(丸)が表示される |
| focus | 外側に 2px のアウトライン |
| エラー | 枠がエラー色 |
| disabled | 背景がグレー、枠が破線、ラベルの文字色も薄くなる |
display:noneで隠すとキーボードで操作できなくなる
チェックボックスの装飾でよく見かけるのが、input を display: none で隠し、隣の要素に見た目を描く方法です。マウスでは問題なく動きますが、キーボードでは操作できなくなります。3つの書き方を並べて確かめました。
<input id="first" type="text">
<!-- 1. display:none で隠した版 -->
<label class="ng"><input id="ng" type="checkbox"><span>display:none で隠した版</span></label>
<!-- 2. 見た目だけ隠した版 -->
<label class="ok"><input id="ok" type="checkbox"><span>見た目だけ隠した版</span></label>
<!-- 3. appearance:none の版 -->
<input id="ap" type="checkbox" style="appearance:none;width:20px;height:20px;border:2px solid #6b7280">
<button id="last">送信</button>
.ng input { display: none; }
.ok input {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.ok input:focus-visible + span::before { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.ng span::before, .ok span::before {
content: "";
display: inline-block;
width: 1em; height: 1em;
margin-right: 6px;
border: 2px solid #6b7280;
vertical-align: -3px;
}
.ng input:checked + span::before,
.ok input:checked + span::before { background: #1d4ed8; }
最初の入力欄から Tab キーを4回押し、フォーカスが移った先を記録した結果です。
Tab順: ok -> ap -> last -> BODY
display: none の版(ng)にはフォーカスが一度も移りませんでした。 JavaScript から focus() を呼んでもフォーカスは移らず、Space キーを押しても checked は false のままです。支援技術に渡される情報(アクセシビリティツリー)を出力すると、次のようになりました。
- textbox
- text: display:none で隠した版
- checkbox "見た目だけ隠した版" [checked]
- text: 見た目だけ隠した版
- checkbox
- button "送信"
1つ目はただの文字として扱われ、チェックボックスとして認識されていません。マウスでクリックした場合は checked: true になるため、マウスだけで確認していると気づきにくい点です。
| 隠し方 | マウス | Tab で移動 | Space で切り替え | 読み上げでの扱い |
|---|---|---|---|---|
display: none |
できる | できない | できない | チェックボックスとして認識されない |
visibility: hidden |
できる | できない | できない | 認識されない(一般に知られた動き。今回は未実行) |
| 見た目だけ隠す(1px に切り抜く) | できる | できる | できる | 認識される |
appearance: none(B の方法) |
できる | できる | できる | 認識される |
直し方は、display: none を「見た目だけ隠す」指定に置き換えるか、B の appearance: none の書き方に切り替えることです。あわせて :focus-visible のスタイルを付け、いまどこにフォーカスがあるかを見えるようにします。
ボタンのデザイン
<button class="f-btn" type="submit">送信する</button>
<button class="f-btn f-btn--sub" type="reset">入力を消す</button>
<button class="f-btn" type="button" disabled>送信できません</button>
/* ===== 7. ボタン ===== */
.f-btn {
min-block-size: 44px;
padding: 8px 20px;
font-size: max(16px, 1rem);
font-weight: 700;
color: var(--f-on-accent);
background-color: var(--f-accent);
border: 2px solid var(--f-accent);
border-radius: var(--f-radius);
transition: background-color 0.15s, transform 0.05s;
}
.f-btn:hover { background-color: color-mix(in srgb, var(--f-accent) 85%, var(--f-text)); }
.f-btn:active { transform: translateY(1px); }
.f-btn:focus-visible { outline: 2px solid var(--f-accent); outline-offset: 2px; }
.f-btn:disabled {
color: var(--f-disabled-text);
background-color: var(--f-disabled-bg);
border-color: var(--f-disabled-bg);
}
.f-btn--sub { color: var(--f-accent); background-color: transparent; }
.f-btn--sub:hover { background-color: color-mix(in srgb, var(--f-accent) 12%, transparent); }
@media (prefers-reduced-motion: reduce) {
.f-input, .f-select, .f-check, .f-radio, .f-btn,
.f-check::before, .f-radio::before { transition: none; }
}
| 状態 | 見た目 |
|---|---|
| 通常 | 背景 rgb(29, 78, 216)、文字 rgb(255, 255, 255) |
| hover | color-mix() でアクセント色に文字色を 15% 混ぜ、少し濃くする |
| active | 1px 下に動かして押した感触を出す |
| focus | 外側に 2px のアウトライン |
| disabled | 背景 rgb(229, 231, 235)、文字 rgb(75, 85, 99)、カーソル not-allowed |
hover の色を color-mix() で作っているので、--f-accent を変えると hover の色も自動で付いてきます。最後の prefers-reduced-motion は、OS で「動きを減らす」を設定している人には変化の動きを付けない、という指定です。
:focus-visibleでフォーカス枠を出し分ける
:focus はクリックでもキーボードでも当たります。:focus-visible は、ブラウザが「フォーカス位置を見せたほうがよい」と判断したときだけ当たります。
ボタンをマウスでクリック { focus: true, focusVisible: false, outline: 'none 0px' }
ボタンへTabキーで移動 { focus: true, focusVisible: true, outline: 'solid 2px' }
inputをマウスでクリック { focus: true, focusVisible: true, outline: 'solid 2px' }
| 操作 | :focus | :focus-visible |
|---|---|---|
| ボタンをマウスでクリック | 当たる | 当たらない |
| ボタンへ Tab キーで移動 | 当たる | 当たる |
| 入力欄をマウスでクリック | 当たる | 当たる(文字を入力する部品のため) |
ボタンをクリックしたときに枠が残るのが気になって outline: none を指定すると、キーボードで操作する人がフォーカス位置を見失います。outline: none で消すのではなく、:focus-visible に枠を付ける形にすると、マウス操作では枠が出ず、キーボード操作では枠が出る、という両立ができます。
:user-invalidでエラー表示のタイミングを整える
:invalid は、条件を満たしていない入力欄にページを開いた瞬間から当たります。required の付いた空欄がすべて最初からエラー色になってしまうので、入力を終えてから当たる :user-invalid を使います。メールアドレス欄での変化を記録しました。
| タイミング | :invalid | :user-invalid | 枠線の色 | エラー文 |
|---|---|---|---|---|
| ページを開いた直後 | true | false | rgb(107, 114, 128) | 非表示 |
| 「abc」と入力している途中 | true | false | rgb(107, 114, 128) | 非表示 |
| フォーカスを外した後 | true | true | rgb(185, 28, 28) | 表示 |
| 正しいアドレスに直した後 | false | false | 元に戻る | 非表示 |
入力の途中では何も出ず、フォーカスを外した時点で初めてエラー表示になります。 また、すべて空のまま送信ボタンを押したところ、name・email・topic・body・agree の5つすべてに :user-invalid が当たりました。「送信しようとした」ことも、ユーザーの操作として扱われます。
サーバー側の検証結果を表示するときは、HTML に aria-invalid="true" を付けます。この記事の CSS は :user-invalid と [aria-invalid="true"] の両方に同じスタイルを当ててあるので、どちらの場合も同じ見た目になります。
iOSの16pxズームへの対応
iOS の Safari では、文字サイズが 16px より小さい入力欄にフォーカスすると、画面が自動で拡大される動きが広く知られています。この記事の CSS では、次の1行で対応しています。
.f-input, .f-select { font-size: max(16px, 1rem); }
max() は大きいほうを選ぶ関数です。ページの基準の文字サイズ(1rem)が 14px のように小さく設定されていても、入力欄は 16px を下回りません。Chromium では入力欄の文字サイズが 16px と取り出せることを確認しました。iOS の実機で拡大が起きないことの確認は、今回の環境では未実行です。
| 書き方 | 1rem が 14px のとき | 1rem が 18px のとき |
|---|---|---|
font-size: 14px |
14px(拡大の対象になる) | 14px |
font-size: 1rem |
14px(拡大の対象になる) | 18px |
font-size: max(16px, 1rem) |
16px | 18px |
viewport の設定に maximum-scale=1 や user-scalable=no を入れて拡大そのものを止める方法は、文字を大きくして読みたい人の操作まで制限してしまうので、文字サイズの側で対応するのがおすすめです。
ダークモードとコントラスト比
冒頭の変数を light-dark() で定義してあるので、部品ごとのダークモード用の CSS は1行もありません。配色をダークにしたときの値です。
| 対象 | ライト | ダーク |
|---|---|---|
| ページの文字 / 背景 | rgb(31, 41, 55) / rgb(255, 255, 255) | rgb(243, 244, 246) / rgb(17, 24, 39) |
| 入力欄の枠線 | rgb(107, 114, 128) | rgb(156, 163, 175) |
| 送信ボタンの文字 / 背景 | rgb(255, 255, 255) / rgb(29, 78, 216) | rgb(17, 24, 39) / rgb(147, 197, 253) |
| disabled の文字 / 背景 | rgb(75, 85, 99) / rgb(229, 231, 235) | rgb(209, 213, 219) / rgb(55, 65, 81) |
| accent-color | rgb(29, 78, 216) | rgb(147, 197, 253) |
ページ全体の背景と文字色も合わせる場合は、body に次のように書きます。
body { color: var(--f-text); background: var(--f-bg); }
コントラスト比
コントラスト比は、2つの色の明るさの差を 1〜21 の数値で表したものです。WCAG(Web コンテンツのアクセシビリティ指針)では、文字は 4.5 以上、入力欄の枠線やフォーカス枠のような文字以外の部品は 3 以上が目安とされています。この記事の配色を計算式どおりに求めた結果です。
| 組み合わせ | ライト | ダーク | 目安 |
|---|---|---|---|
| 文字 / 背景 | 14.68 | 16.12 | 4.5 以上 |
| 補足・placeholder / 背景 | 7.56 | 12.04 | 4.5 以上 |
| 枠線 / 背景 | 4.83 | 6.99 | 3 以上 |
| フォーカス枠・アクセント色 / 背景 | 6.70 | 9.84 | 3 以上 |
| ボタンの文字 / ボタンの背景 | 6.70 | 9.84 | 4.5 以上 |
| エラー文 / 背景 | 6.47 | 9.35 | 4.5 以上 |
| disabled の文字 / disabled の背景 | 6.10 | 7.00 | (対象外だが読める濃さに) |
枠線の色は見落としやすい箇所です。白い背景に対して、明るいグレー #d1d5db は 1.47、#9ca3af は 2.54 で、どちらも 3 に届きません。入力欄の範囲が枠線だけで示されているデザインでは、#6b7280(4.83)程度の濃さにしておくと、明るい場所でも入力欄を見つけやすくなります。placeholder も同じ理由で、ブラウザ標準の薄い色のままにせず、補足文と同じ色を指定しています。
対応ブラウザ表
npm の @mdn/browser-compat-data 8.1.4(2026-10-01 付)から取り出した、最初に対応した版です。
| 機能 | Chrome / Edge | Firefox | Safari / iOS Safari | 対応していない場合 |
|---|---|---|---|---|
:focus-visible |
86 | 85 | 15.4 | フォーカス枠が出ない(:focus も併記すると出せる) |
:user-invalid |
119 | 88 | 16.5 | エラー色にならない(aria-invalid 側は効く) |
accent-color |
93 | 92 | 15.4 | ブラウザ標準の色のまま |
appearance(接頭辞なし) |
84 | 80 | 15.4 | 標準の見た目が残る |
field-sizing |
123 | 152 | 26.2 | 高さが固定(最小の高さで表示) |
light-dark() |
123 | 120 | 17.5 | 色の変数が無効になる |
color-scheme |
81 | 96 | 13 | - |
:has() |
105 | 121 | 15.4 | エラー文が表示されない |
color-mix() |
111 | 113 | 16.2 | hover の色が変わらない |
lh 単位 |
109 | 120 | 16.4 | textarea の最小の高さが効かない |
:where() |
88 | 78 | 14 | リセットが効かない |
accent-color には、Safari 15.4〜26.1 と Android 版 Chrome について「チェックマークが見やすくなるようなコントラストの自動調整を行わない」という注記があります。これらの環境では、チェックマークがはっきり見える濃さのアクセント色を選んでおくと安心です。
もっとも新しい条件が必要なのは light-dark() です。それより前の版も対象にするときは、変数の定義だけを次の形に置き換えます(この書き換え版は今回の検証の対象外で、未実行です)。
:root {
color-scheme: light dark;
--f-bg: #ffffff;
--f-text: #1f2937;
/* 残りの変数もライトの値で定義 */
}
@media (prefers-color-scheme: dark) {
:root {
--f-bg: #111827;
--f-text: #f3f4f6;
/* 残りの変数をダークの値で上書き */
}
}
つまずきやすい点と対処
| 起きること | 理由 | 対処 |
|---|---|---|
| 入力欄だけ文字が小さく、フォントも違う | フォーム部品は周りの文字設定を引き継がない | font: inherit |
スタイルが上書きできず !important が増える |
リセットや共通ルールの詳細度が高い | リセットを :where() で包む |
width: 100% にすると枠からはみ出す |
余白と枠線が幅に足される | box-sizing: border-box |
| ページを開いた時点で入力欄がエラー色 | :invalid は最初から当たる |
:user-invalid に置き換える |
| クリックのたびにボタンに枠が残る | :focus はマウス操作でも当たる |
:focus-visible に枠を付ける |
| チェックボックスに Tab で移動できない | display: none で input を隠している |
見た目だけ隠す、または appearance: none で装飾する |
| select の矢印がダークモードで見えにくい | 矢印を固定の色の画像で描いている | currentColor を使ったグラデーションで描く |
| iOS で入力時に画面が拡大される | 入力欄の文字が 16px より小さい | font-size: max(16px, 1rem) |
| placeholder が薄くて読みにくい | ブラウザ標準の色が薄い | ::placeholder に色を指定し opacity: 1 |
| disabled の文字が読めない | 透明度を下げすぎている | 背景と文字の色を個別に決める |
よくある質問
Q. フォームのデザインは、どこから手を付ければよいですか?
A. リセット(font: inherit、box-sizing: border-box)を最初に置き、次に input・textarea・select に共通の枠・余白・文字サイズを決め、最後に hover・focus・エラー・disabled の状態を足していきます。この順番にすると、部品ごとの差分だけを書けば済みます。
Q. チェックボックスの色を変えるだけなら、いちばん短い書き方は何ですか?
A. accent-color: 色; の1行です。input 要素か、その親要素に指定します。形や大きさ、チェックマークまで決めたいときは、appearance: none で標準の見た目を外して作り込みます。
Q. :focus と :focus-visible はどちらを使えばよいですか?
A. フォーカス枠には :focus-visible を使います。ボタンをマウスでクリックしたときは当たらず、Tab キーで移動したときと、入力欄にフォーカスしたときに当たることを確かめました。枠を outline: none で消すだけにすると、キーボード操作でフォーカス位置がわからなくなります。
Q. エラーの見た目は、色を赤くするだけで十分ですか? A. 色に加えて、枠を太くする、エラー文を表示する、といった色以外の手がかりも付けると、より多くの人に伝わります。この記事の CSS では、枠線の色・1px の影・エラー文の表示の3つを同時に切り替えています。
まとめ
- 「リセット → 共通の見た目 → 状態」の順に重ねると、6部品を1つの CSS でそろえられる
- リセットを
:where()で包むと詳細度が 0 になり、!importantなしで上書きできる - チェックボックスの色だけなら
accent-colorの1行、形まで決めるならappearance: noneで input 自体を装飾する display: noneで input を隠すと、Tab でも Space でも操作できなくなる。input は残したまま装飾する- フォーカス枠は
:focus-visible、エラー表示は:user-invalidにすると、出るタイミングが自然になる field-sizing: contentで伸びる textarea、font-size: max(16px, 1rem)で iOS の拡大への対応ができる- 色は
light-dark()の変数にまとめ、コントラスト比は文字 4.5 以上・枠線 3 以上を目安に決める
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSSフォームデザイン|input・select・チェックボックス装飾」
株式会社RJCでは、CSSのフォームデザインをはじめとする画面づくりの知識を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。