CSSで、チェックボックスを好きな見た目にする方法

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

チェックボックスの色を変えるだけなら、accent-color を1行書けば済みます。枠の形や角の丸み、チェックの形まで変えたいときは、appearance: none でブラウザーの見た目を消し、チェックボックスそのものに枠を描いて、チェックの形を ::before で出します。要素は <input type="checkbox"> のままなので、クリック、Space キー、フォームで送る値は、普通のチェックボックスと変わりません。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、マウスとキーボードで操作して、getComputedStyle() の値、スクリーンショットの画素の色、アクセシビリティツリーを記録しました。強制カラーモードは Playwright の forcedColors: 'active' で再現したもので、Windows のハイコントラストモードの実機は使っていません。Firefox と Safari、スクリーンリーダーでの実際の読み上げも試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

「お知らせのメールを受け取る」のラベルの文字を3回クリックする。クリックするたびに、左のチェックボックスが、白地に濃い水色の枠(オフ)と、濃い水色の塗り(オン)に交互に切り替わり、白いチェックが大きくなって現れたり、小さくなって消えたりする。下の表示が「checked:true」「checked:false」に切り替わる
ラベルの文字を3回クリックしたときの動き。切り替わる動きは、見やすいように実際の約4倍の時間をかけて再生している(上の回数表示、マウスの絵、押した位置のピンクの輪、下の checked の表示は GIF 用に足したもの)

色を変えるだけなら accent-color の1行で済む

css
.accent {
  accent-color: #067a98;
}

accent-color は、チェックボックスなどのフォーム部品の強調色を決めるプロパティです。検証環境で、チェックしたチェックボックスのスクリーンショットの画素を色ごとに数えると、何も指定しないときにいちばん多い色は #0075ff(青)で、accent-color: #067a98 を付けると #067a98 になりました。

チェックの色は、ブラウザーが決めます。#067a98 のような濃い色ではチェックが白になり、#00c8e8 のような明るい色では、チェックが濃い灰色(#3b3b3b)に変わりました。塗りの色が明るくても、チェックが見えなくなることはありませんでした。

上から3つのチェック済みのチェックボックス。1つ目は何も指定しない青。2つ目は accent-color を濃い水色にしたもので、チェックは白。3つ目は accent-color を明るい水色にしたもので、チェックは濃い灰色
accent-color の違い。見やすいように、3つとも幅と高さを 22px にして撮った(各行の見出しの文字は画像用に足したもの)

大きさは、何も指定しないと 13px 四方でした。width: 22px; height: 22px を付けると 22px 四方になり、accent-color の色もそのまま使われました。色と大きさを変えれば十分なら、ここで終わりです。

互換性データでは、accent-color は Chrome 93、Firefox 92 から使えます。Safari は 15.4 から使えますが、26.2 より前は一部の対応で、「読みやすさのための最低限のコントラストを保たない」という注記が付いています。

appearance: none で、チェックボックスそのものに描き直す

枠を角丸にしたい、チェックの形を変えたい、というときは、ブラウザーの見た目を消して描き直します。HTML は、label の中にチェックボックスと文字を入れるだけです。

html
<label class="row">
  <input class="check" type="checkbox" name="news">
  お知らせのメールを受け取る
</label>
css
.row {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
}

/* 見た目を描き直す:チェックボックスそのものに枠とチェックを描く */
.check {
  appearance: none; /* ブラウザーの見た目を消す */
  display: inline-grid;
  place-content: center;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  margin: 0;
  border: 2px solid #067a98;
  border-radius: 6px;
  background: #fff;
  cursor: pointer;
  transition: background-color 120ms ease-out;
}

/* チェックの形(はじめは大きさ 0 で見えない) */
.check::before {
  content: "";
  width: 12px;
  height: 12px;
  background: #fff;
  clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 22%, 86% 8%, 38% 70%);
  transform: scale(0);
  transition: transform 120ms ease-out;
}

/* チェックされたとき:塗りつぶして、チェックを出す */
.check:checked {
  background: #067a98;
}

.check:checked::before {
  transform: scale(1);
}

MDN の appearance の説明にあるとおり、appearance: none はブラウザー固有の見た目を消すだけで、要素の機能は変わりません。検証環境でも、22px 四方の白い四角に濃い水色の枠が付いた見た目になり、チェックボックスをクリックしたときも、ラベルの文字をクリックしたときも、チェックが切り替わって change イベントが起きました。

チェックの形は、チェックボックスの ::before に 12px 四方の白い四角を置き、clip-path: polygon() でチェックの形に切り抜いて作っています。チェックボックス自体を display: inline-grid と place-content: center にしているので、::before は真ん中に来ます。検証環境の Chromium では、appearance: none のチェックボックスに ::before が描かれました。チェックした後のスクリーンショットでも、塗りの内側に塗りと違う色の画素が 56 個あり、チェックが描かれていることを確かめました。

チェックの出し入れは、::before の大きさを scale(0) と scale(1) で切り替えています。次の表は、transition を一時停止し、時間を 30ms ずつ進めて読んだ値です。

経過時間 チェックの大きさ(倍) 塗りの色
0ms 0 rgb(255, 255, 255)
30ms 0.38 rgb(161, 205, 216)
60ms 0.68 rgb(85, 164, 184)
90ms 0.91 rgb(29, 134, 162)
120ms 1 rgb(6, 122, 152)

チェックが小さい点から大きくなり、塗りの色も同じ 120ms で白から濃い水色に変わりました。

互換性データでは、接頭辞なしの appearance は Chrome 84、Firefox 80、Safari 15.4 から使えます。

未確定の状態は、:indeterminate で横線にする

「すべて選ぶ」のチェックボックスで、一部だけが選ばれている状態を見せたいときは、未確定(indeterminate)の状態を使います。MDN のチェックボックスの説明にあるとおり、この状態は HTML の属性では付けられず、JavaScript で indeterminate プロパティを true にします。

js
checkbox.indeterminate = true;

MDN のチェックボックスの説明によると、未確定のとき、多くのブラウザーはチェックの代わりに横線を描きます。appearance: none はこうしたブラウザーの見た目を消すので、:indeterminate で自分で描きます。チェックの ::before を高さ 3px にして、clip-path を外すと、白い横線になります。

css
/* 未確定(indeterminate)のとき:横線を出す */
.check:indeterminate {
  background: #067a98;
}

.check:indeterminate::before {
  height: 3px;
  clip-path: none;
  transform: scale(1);
}

検証環境で indeterminate を true にすると、:indeterminate が当てはまり、塗りが濃い水色、::before が 12px × 3px になりました。アクセシビリティツリーには checkbox "すべて選ぶ" [checked=mixed] と出て、見た目を描き直しても、一部だけ選ばれた状態として伝わっています。

未確定の状態は、見た目だけのものです。checked は false のままなので、FormData を読むと、このチェックボックスの値は入っていませんでした。未確定のチェックボックスをクリックすると、indeterminate は false に戻り、checked が true になって、FormData に all=on が入りました。

フォーカスの枠は、チェックボックスそのものに付ける

css
/* キーボードで選んだときだけ、外側に枠を出す */
.check:focus-visible {
  outline: 2px solid #d85878;
  outline-offset: 2px;
}

appearance: none で描き直すと、見た目を描く要素と、フォーカスが当たる要素が同じ input になります。そのため、フォーカスの枠も、チェックボックス自身に .check:focus-visible で付けるだけで済み、ほかの要素に枠を移す必要はありません。

検証環境で Tab キーを押してチェックボックスを選ぶと、:focus-visible が当てはまり、チェックボックスの外側に 2px の枠が出ました。そのまま Space キーを押すとオンになって change イベントが起き、Enter キーでは変わりませんでした。マウスでクリックしたときは、:focus-visible は当てはまらず、枠は出ませんでした。

上から5つのチェックボックス。オフは白い四角に濃い水色の枠。オンは濃い水色の塗りに白いチェック。未確定は濃い水色の塗りに白い横線。Tab キーで選んだときは、オフの四角の外側にピンクの枠。押せないときは、灰色の塗りに白いチェック
記事の CSS のチェックボックスを、実際に操作して撮ったもの。上から、オフ、クリックした後のオン、未確定、Tab キーで選んだとき、押せないとき(各行の見出しの文字は画像用に足したもの)

押せないときの見た目も、自分で描く

css
/* 押せないとき */
.check:disabled {
  border-color: #9aa5ab;
  background: #eef1f2;
  cursor: not-allowed;
}

.check:disabled:checked {
  background: #9aa5ab;
}

checked と disabled を付けたチェックボックスは、塗りが灰色(rgb(154, 165, 171))になりました。ラベルをクリックしてもチェックは変わらず、change イベントも起きませんでした。アクセシビリティツリーでは [checked] [disabled] が付きました。

強制カラーモードでは、チェックの色を CanvasText にする

Windows のハイコントラストモードのような強制カラーモードでは、ページの色がシステムの色に置き換わります。MDN の forced-colors の説明によると、background-color や border-color などは、ページで指定した値の代わりに、ブラウザーが選んだシステムの色で描かれます。

チェックの形は ::before の背景色で描いているので、ここが問題になります。Playwright で強制カラーモードを再現すると、塗りの色も ::before の色も白(rgb(255, 255, 255))になりました。オフ、クリックしてオンにしたもの、未確定のどれも、枠の内側に白以外の画素が1つもありませんでした。見た目では、どの状態か分からなくなります。

そこで、強制カラーモードのときだけ、チェックの色をシステムの文字の色(CanvasText)にします。

css
/* 強制カラーモード:チェックの色をシステムの文字色にする */
@media (forced-colors: active) {
  .check::before {
    background: CanvasText;
  }
}

これを足すと、::before の色は rgb(0, 0, 0) になり、クリックしてオンにしたものではチェックが、未確定のものでは横線が、黒で描かれました。

強制カラーモードのとき オフ オン 未確定
指定なし 中は白いまま 中は白いまま 中は白いまま
CanvasText を指定 中は白いまま 黒いチェック 黒い横線
強制カラーモードで撮った6つのチェックボックス。上半分は指定なしで、オフ、オン、未確定の3つがどれも黒い枠の白い四角に見える。下半分は CanvasText を指定したもので、オンには黒いチェック、未確定には黒い横線がある
Playwright で強制カラーモードを再現して撮ったもの。上は強制カラーモード用の指定なし、下は CanvasText を指定したとき(見出しの文字は画像用に足したもの)

accent-color だけで色を変えたチェックボックスは、強制カラーモードでは濃い紫(#37006e)に塗られ、その中にチェックが描かれていました。チェックが消えたのは、appearance: none で描き直したチェックボックスのほうです。

動きを減らす設定では、transition を外す

チェックが大きくなる動きは、動きを減らす設定のときには止めます。

css
@media (prefers-reduced-motion: reduce) {
  .check,
  .check::before {
    transition: none;
  }
}

Playwright でこの設定にしてクリックすると、チェックはクリックの直後から元の大きさで描かれ、動いているアニメーションは 0 個でした。

​Posted by 86