CSSで、必須の入力欄のラベルの後ろに「*」を出す方法

​​CSS​HTML,Playwright,アクセシビリティ,フォーム

HTML の入力欄に required を付けておけば、CSS の :has(:required) で必須の欄を含む要素を選び、その中のラベルの後ろに ::after で「」を出せます。必須かどうかは required 属性だけで決まるので、ラベルごとに「」を手で書き足す必要はありません。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、getComputedStyle() で ::after の値を、Chrome DevTools Protocol で入力欄のアクセシブルな名前と必須かどうかを記録しました。Firefox と Safari、スマートフォンのブラウザー、実際のスクリーンリーダーでの読み上げは試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

白い背景のフォーム。いちばん上に「* の付いた項目は入力が必要です。」と書いてある。その下に「お名前」「メールアドレス」「会社名」のラベルと入力欄が縦に並び、「お名前」と「メールアドレス」のラベルの後ろにだけ、ピンクの「*」が付いている。「会社名」には付いていない
この記事の CSS で、required を付けた2つの欄のラベルにだけ「*」が出たところ

HTML は、ラベルと入力欄を1つの要素で包む

ラベルと入力欄を、field というクラスの <div> で包みます。必須の欄の入力欄には required を付けます。

html
<form>
  <p class="note">* の付いた項目は入力が必要です。</p>
  <div class="field">
    <label for="name">お名前</label>
    <input id="name" name="name" required>
  </div>
  <div class="field">
    <label for="email">メールアドレス</label>
    <input id="email" name="email" type="email" required>
  </div>
  <div class="field">
    <label for="company">会社名</label>
    <input id="company" name="company">
  </div>
</form>

フォームの先頭には「* の付いた項目は入力が必要です。」という文を置きました。「」だけでは、何を表すのかが画面を見る人に伝わりにくいためです。MDN の :required のページの例でも、「」の意味を説明する文をフォームに置いています。

CSS は、必須の欄を含む要素のラベルに ::after を付ける

css
.field {
  display: grid;
  gap: 4px;
  margin-bottom: 16px;
}

.field:has(:required) > label::after {
  content: "*";
  content: "*" / "";
  margin-left: 4px;
  color: #d85878;
  font-weight: 700;
}

:required は、required が付いた入力欄に当てはまります。:has() を使うと、指定した要素を中に含む親の要素を選べます。.field:has(:required) で「必須の入力欄を含む .field」を選び、その直下の label の ::after に「*」を入れています。

検証環境で getComputedStyle() を使って調べると、「お名前」と「メールアドレス」のラベルの ::after は content が "*" / ""、色が rgb(216, 88, 120)(#d85878)でした。required のない「会社名」のラベルは、content が none で、何も出ていません。

content を2行書いている理由は、後の「*」を読み上げさせない書き方の節で説明します。

required を後から付け外ししても、「*」の表示が切り替わる

JavaScript で「お名前」の欄の required を外すと、「お名前」のラベルの ::after は content: none になりました。反対に「会社名」の欄に required を付けると、「会社名」のラベルの ::after が "*" / "" になりました。CSS の側は何も変えていません。選択した内容によって必須の欄が変わるフォームでも、required を付け外しするだけで「*」の表示も切り替わります。

入力欄の ::after に書いても表示されない

「*」を入力欄そのものに付けようとして、input:required::after に content を書いても、画面には何も出ません。

2つの薄い水色の枠が縦に並んでいる。上の枠は「input:required::after に書いたとき」という見出しで、「お名前」のラベルと入力欄があるが、どこにも「*」は出ていない。下の枠は「ラベルの ::after に「必須」と書いたとき」という見出しで、「お名前」のラベルの後ろにピンクの角丸の四角に白い文字で「必須」と出ている。その下の「会社名」には何も付いていない
上は入力欄の ::after に「*」を書いたもの、下はラベルに「必須」の文字を出したもの

検証環境で、input:required::after { content: "*"; } と書いた入力欄を調べました。getComputedStyle() で読むと ::after の content は "*" でしたが、この指定がある場合とない場合で、入力欄のスクリーンショットはまったく同じでした。値は計算されていますが、画面には描かれていません。

MDN の ::after のページには、<img> のような置換要素には ::after を適用できないと書かれています。入力欄についての記述はありませんが、検証環境でも入力欄の ::after は描かれませんでした。入力欄の近くに出したい表示は、ラベルや、入力欄を包む要素の側に付けます。

「必須」の文字にするときも、セレクターは同じ

「*」ではなく「必須」の文字を出したいときは、content の文字を変えて、背景色などを足します。比べた画像の下側の枠は、次の CSS で作りました。

css
.field:has(:required) > label::after {
  content: "必須";
  content: "必須" / "";
  margin-left: 8px;
  padding: 0 6px;
  border-radius: 4px;
  background: #d85878;
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  vertical-align: 2px;
}

MDN の :required のページでは、任意の欄もあるフォームで、必須の欄を色だけで示さないように勧めています。「*」や「必須」の文字を出し、枠の色を変えるだけで済ませないようにします。

「*」を読み上げさせない

::after で入れた文字は、入力欄のアクセシブルな名前に入ることがあります。検証環境で、content: "*" の1行だけを書いたページを調べると、「お名前」の入力欄の名前は「お名前*」になっていました。MDN の content のページには、生成コンテンツは、支援技術とブラウザーの組み合わせによってはアナウンスされないことがあると書かれています。少なくとも Chromium 153 では、ラベルの文字と一緒に「*」が名前に入りました。

同じ MDN のページにあるとおり、content は、スラッシュ(/)の後に代替テキストを書けます。代替テキストの値は、ブラウザーのアクセシビリティツリーに渡されます。この記事の CSS では、代替テキストを空の文字列 "" にしました。検証環境では、入力欄の名前は「お名前」になり、「*」は入りませんでした。「必須」の文字にしたときも、名前は「お名前」でした。

必須であることは、required 属性の側で示されます。検証環境で、required を付けた入力欄はどれも、アクセシビリティツリーで required が true になっていました。MDN の :required のページにも、required 属性を付けることで、支援技術を使う人がどの欄が必須かを理解できると書かれています。

代替テキストを書けないブラウザーのために、1行目を残す

互換性データによると、content の代替テキストは Chrome 77、Edge 79、Firefox 128、Safari 17.4 から使えます。それより古いブラウザーでは、content: "*" / ""; の行を解釈できません。MDN の CSS のエラー処理のページにあるとおり、ブラウザーは解釈できない宣言を無視します。:has() に対応しているブラウザーなら、1行目の content: "*"; が残り、「*」は画面に出ます。

:has() は、互換性データでは Chrome 105、Edge 105、Firefox 121、Safari 15.4 から使えます。同じ MDN のページによると、対応していないセレクターがあると、そのルール全体が無視されます。:has() を使えないブラウザーでは、「*」は出ません。Chrome 77 や Edge 79 より前のブラウザーは、:has() にも対応していません。そのため1行目が効くのは、:has() には対応していて代替テキストには対応していないブラウザー(互換性データでは Firefox 121〜127、Safari 15.4〜17.3)だけです。

​Posted by 86