こなさんみんばんわ。
もう先輩どころか「長老」とか呼ばれるような年齢です。涙

あっ長老といえば👇もうすぐニューアルバムが出るんですね。ていうか、この記事をみなさんが読む頃にはすでにリリース後だったりするのかもしれません。そんなギリギリで紹介してるようではいかんな。涙

そんな訳で、まったくポルカとは無関係な本日のお題にまいります。

CSS で独自のチェックボックスやラジオボタンを作る時の話

Web 制作をしていると、フォーム部品のチェックボックス やラジオボタン の見た目を UA デフォルトのものから変更したい場面があります。みなさんはどのように実装されていますか? まぁ具体的な手法は人それぞれでしょうが、雑にまとめてしまうといずれの方法も、おおむね次の 2 つで説明がつくかと思います。

  1. 何らかの方法を使って元々のチェックボックスやラジオボタンを消す
  2. 何らかの要素を使って独自のチェックボックスやラジオボタンを作る

このうち 1. についてはさすがに今はもうdisplay: none で消すとアクセシビリティ的によろしくない1」というのが一般常識として知れ渡っておりますので、今もこの方法で消してるという方はほぼいないと思います(やってないですよね? 絶対ダメですよ🙅🏻‍♂️)。

で、その代替としてはこれまで色々な方法…たとえば opacity: 0 で透過とか、いわゆる Visually hidden を使って隠すなどが使われてきました。そして、ここ最近は appearance: none で UA デフォルトの見た目を削除して見えなくするという方法もよく使われるようになってきたように思います。まぁ appearance をサポートしないブラウザがなくなりましたので、配慮しなくてもよくなったってのは大きいですかね。

ていうか、そこまでやるなら <input> の擬似要素使いましょうよ

そんな訳で、チェックボックスやラジオボタンを隠すのに appearance: none が使えることはよく知られるようになってきましたが、一方そうやってデフォルトの見た目を消したチェックボックス・ラジオボタンでは実は ::before / ::after 擬似要素が使える2というのはまだまだ知られていない…のかどうかはよく分かりませんが😅 少なくともネットで検索して見つかるチェックボックス・ラジオボタンのカスタム系記事上位をざっと見渡した限りはあまりそのことに触れてる記事は多くなく、まだまだ <input> 要素に隣接した <label><span> の擬似要素で独自のデザインを作るといった解説記事の方が多いように感じます3

まぁネット検索上位の結果が世の中を正しく反映している訳ではないですし、ちゃんとした現場では普通に <input> の擬似要素が使われているのかもしれませんが、いわゆる初学者の方々はこういった検索して出てきた記事を参考に学習するかもしれない訳で、このままではそういった方々にこのような古いテクニックを身に付けさせてしまうことになりますので、それはいささか残念な気がします。

ですので、ここで記事を書くことで <label> とかの擬似要素を使うのは古いよ! <input> の擬似要素使った方が何かと便利だし楽だよ! というのをもっと世の中に広めていきたいなーと思っております。

<input> の擬似要素を使うメリット 4 つ

では <input> の擬似要素を使うのは、<label><span> のものを使うのに比べて何が便利なのでしょう? メリットが多ければ積極的に使う理由になると思いますので、そのあたりを説明していきます。

1. マークアップの制限を受けない

今までのチェックボックス・ラジオボタンのカスタマイズ方法は :checked 擬似クラスと隣接セレクタの組み合わせでスタイルを変更するという関係上、どうしても <input> 要素とテキストラベルの要素(ここの ::before / ::after 擬似要素が使われる)が同じ親要素内で隣り合っている必要があります。これは <label><input> を囲うか否かでマークアップが変わってしまうということでもあります4

<!-- 最初はこう書いてたのに… -->
<input type="checkbox" id="foo" …>
<label for="foo">テキストラベル</label>

<!-- あとで「こっちに統一」と言われた… -->
<label>
  <input type="checkbox" …>テキストラベル
</label>

<!-- なので span を足さないといけない… -->
<label>
  <input type="checkbox" …>
  <span>テキストラベル</span>
</label>

場合によっては CSS の書き換えも発生しますので大変ですね。でも <input> 要素とその擬似要素だけを使っていれば、そのスタイルは <input> 要素にしか紐付かないので、それ以外のマークアップ変更からは何の影響も受けません<label> で囲われようが for 属性で紐付けられようが、もっといえば <label> そのものがなくなろうが問題なしです🤣(ちゃんと書きましょうね)

/* 
 *  label 要素がどこにあろうがここのスタイルは関係なし!
 */
input[type=checkbox] {
  /* チェックボックスの枠スタイル */
}
input[type=checkbox]:checked::before {
  /* チェックマークのスタイル */
}
input[type=checkbox]:disabled {
  /* 無効なチェックボックスのスタイル */
  &:checked::before { … }
}

2. 位置調整に頭を悩ませなくてもいい

元のチェックボックス・ラジオボタンを非表示にして <label> などの ::before / ::after 擬似要素で独自の UI を作る場合、まずは <label> のパディングで要素の配置場所を空け、そこに擬似要素を position: absolute で配置していく感じになると思うのですが、枠線と中身(マーク)の配置調整は当然ながら、テキストラベルとの縦の位置合わせがけっこう大変じゃないですか? 慣れないうちはちょっとずつ top などを調整しながら、いい感じになるようにがんばっていたんじゃないでしょうか。

でも <input> とその擬似要素を使う場合、その縦位置は vertical-align を少し調整するか(11 月 13 日の記事参照)せいぜい Flexbox を使った次のようなスタイルがあれば事足ります。微調整を繰り返す必要もないですし、何よりシンプルです。

/* label > input + テキストノード(無名ボックス)という想定 */
label:has(input:is([type=checkbox], [type=radio])) {
  display: inline-flex;
  align-items: center;  /* 見た感じで問題がなければ `baseline` でも可 */
}

3. 元の <input> 要素のスタイルを活かせる

<label> などの擬似要素で独自のチェックボックス・ラジオボタンを作っても、そのままではキーボード操作時のフォーカスリングが付かないので、新たにフォーカス時の outline を設定する必要がありますが、これをやるための CSS セレクタがパッと出てこない人は割と多いような気がします。実際のところはどうなのか分かりませんが、まぁでも CSS に精通してないと出てこんよね、こんなの。涙

/* 例 1. label で input を囲い span の ::before 擬似要素を使ってチェックボックスの枠を作る場合だと */
label:focus-within > span::before {
  outline: auto;
  outline-color: -webkit-focus-ring-color;
}
/* 例 2. input の横に label を置いてその擬似要素でチェックボックスの枠を作る場合だと */
input[type=checkbox]:focus-visible + label::before {
  /* 例 1 に同じ */
}

でも大丈夫です。<input> 要素を使う方法では <input> 自体に元から設定されているフォーカスリングもそのまま活かすことができます。特に独自のフォーカスリング・スタイルを設定しない、UA デフォルトのままでいいという場合、これは特に有用ではないかと思います。

もちろん独自のスタイルを当てたい場合にも対応できます。input:focus-visible を変更すればいいだけですから。

4. 親子関係に基づく CSS プロパティが使える

はい、ラストの項目であり、かつ一番の便利ポイントはここでしょうか。<input> 要素とその ::before / ::after 擬似要素は、DOM でいえば親子のような関係にありますよね。

<!-- あくまでもイメージです -->
<input type="checkbox">
  <::before content="" />

  <::after content="" />
</input>

これが何を意味するかというと、親子関係というかネストの関係にないと使えない CSS 機能が使えるということです。例えば <input> 要素に color を与えておけば擬似要素で background-color: currentColor のような指定ができます。また通常は継承しないプロパティを inherit で継承したり、新しいところだとコンテナクエリーなんかも使えます。

例えば inherit は独自のラジオボタンを作る場合などに有用でしょう。ラジオボタンは通常、枠も中のマークも正円なので、<input>border-radius に適切な値を与えておけば、擬似要素の側ではただ inherit するだけで済みます。ちなみに例で使っている calc(1px/0) という計算式については、僕が Zenn に書いた記事に解説がありますので、併せてお読みいただけますと幸いです(宣伝)。

/* 一部のコードは意図的に省略しています、完全版は後続のデモにて確認を */
input[type=radio] {
  position: relative;
  border: 1px solid;
  border-radius: calc(1px/0);

  &:checked::before {
    position: absolute;
    inset: 2px;
    border-radius: inherit;  /* 1px * infinity を継承 */
    background-color: currentColor;
  }
}

余談ですが上のコード例、チェックされた時にアニメーションさせるような場合でも inset を調整(50% から 2px など)するだけで済んだりします。楽ちんですね。文珍師匠の一番弟子ちがうよ。あれ、同じネタをつい最近も書いた気がするな。気のせいだろうか🤔

See the Pen Radio Button Demo by Jeffrey Francesco (@jforg) on CodePen.

ラジオボタン以外でも、例えばチェックボックスを使ってトグルスイッチのような UI を作り、チェック時にはスイッチをオン側に移動させるといった用途には、コンテナクエリーで使える cqwcqi といった単位が役に立つでしょう。一度コードを書いておけば、あとでスイッチの幅や高さを変更したくなった場合にも、擬似要素部分のコードは基本そのままでいいので面倒じゃなくていいですね。

/* 一部のコードは意図的に省略しています、完全版は後続のデモにて確認を */
input[type=checkbox] {
  container-type: inline-size;
  position: relative;
  block-size: 16px;
  inline-size: 32px;
  border: 2px solid;
  border-radius: calc(1px/0);

  &::before {
    position: absolute;
    aspect-ratio: 1;
    inset-block: 2px;
    inset-inline: 2px auto;
    border-radius: inherit;
    background-color: currentColor;
  }
  /* チェックされた場合はコンテナ幅から、奥まってるサイズ分 + 擬似要素幅を引いただけ横移動させると、ちょうど反対側に配置される */
  &:checked::before {
    translate: calc(100cqi - (2px * 2 + 100%));
  }
}

以下のデモでは 3 つのスイッチを置いてますが、サイズの違いはカスタムプロパティを設定しているだけで、コード部分はすべて共通になっているのはご覧になってお分かりいただけるかと思います。

See the Pen Toggle Switch Demo by Jeffrey Francesco (@jforg) on CodePen.

いかがですか? このように <input> の擬似要素を使うと、<label><span> の擬似要素を使うよりも便利に、より簡単に独自のチェックボックスやラジオボタンをスタイリングしていくことができるのです。そうと分かればもう使わない理由はありませんね! みんなでどんどん積極的に使っていきましょう。そして過去のテクニックを駆逐していきましょう😊

まとめ

  • appearance: none で UA デフォルトの見た目を消したチェックボックスやラジオボタンでは ::before / ::after 擬似要素が使えます
  • こちらの擬似要素を使えば <label><span> の擬似要素を使うよりも簡単に、独自のチェックボックスやラジオボタンをデザインできます
  • メリットとしては、次の項目が挙げられます
    • マークアップの制限を受けない
    • Flexbox などを使って簡単に位置調整ができる
    • フォーカスリングなど元の <input> 要素のスタイルを活かせる
    • inherit, コンテナクエリーなど、ネストの関係にないと使えない CSS 機能が使える
  • どんどん使っていきましょう

そんな訳で

本日はチェックボックスやラジオボタンをカスタマイズするのに、今では <label><span> の擬似要素を使うより <input> 要素そのもの擬似要素を使った方が便利で簡単だよ、という話でした。

今回はとりあえず積極的に使っていただくことを念頭に書いたので、本来ならば考えないといけない部分…例えば background-color で色付けした要素は強制カラーモードで見えなくなってしまうので云々といった話など…はあえて端折ったところがあります。いちおうデモの方ではその辺も含めてコードを書いておきましたので、そちらを参考にしていただくことでご容赦いただければと思います。

以上、最後までお読みいただいたみなさまのお役に立てましたら幸いです!

  1. スクリーンリーダーに読み上げられない・キーボードで操作不能になる etc. 

  2. 実際のところは Firefox を除けば appearance: none で消さなくても ::before / ::after 擬似要素が使えたりしますが、それはそれ。 

  3. 昔の記事が混じってるんちがうの? と思うでしょうが、ごく最近(1 年以内)に絞ってみても似たような感じです。 

  4. 厳密には隣り合ってなくても後方セレクタ(後続兄弟結合子 ~)を使えばいいのですが、親要素が同じでないといけないのは一緒です。