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); }

仕組みは次のとおりです。

  1. appearance: none で標準の見た目を外し、input 要素そのものに枠と背景を付ける
  2. ::before でチェックマーク(clip-path で切り抜いた形)や丸を用意し、transform: scale(0) で隠しておく
  3. :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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?