CSSだけで、チェックすると取り消し線が付くやることリストを作る方法

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

やることリストの各行を <label> で包み、チェックボックスのすぐ後ろの <span> に input:checked + span で取り消し線を付けると、JavaScript を書かずに「チェックした項目だけ線で消す」リストになります。CSS カウンターを足せば、「2 / 5 件完了」のような終わった数も CSS だけで出せます。

補足

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

5行のやることリスト。「ゴミを出す」と「洗濯物をたたむ」のチェックボックスにチェックが入り、その2行の文字は灰色になって、真ん中に横線が引かれている。ほかの3行は黒い文字のまま。リストの下に濃い水色で「2 / 5 件完了」と出ている
5項目のうち2つをチェックしたところ。チェックした行だけ文字が灰色になり、取り消し線が付く

コード全体

HTML は、1行ごとに <label> の中へチェックボックスと文字を入れます。文字は <span> で包み、チェックボックスのすぐ後ろに置きます。

html
<ul class="todo">
  <li><label><input type="checkbox"> <span>牛乳を買う</span></label></li>
  <li><label><input type="checkbox"> <span>ゴミを出す</span></label></li>
  <li><label><input type="checkbox"> <span>記事の下書きを書く</span></label></li>
  <li><label><input type="checkbox"> <span>猫のトイレを掃除する</span></label></li>
  <li><label><input type="checkbox"> <span>洗濯物をたたむ</span></label></li>
</ul>
<p class="todo-count"></p>

CSS は次のとおりです。終わった数を出す部分(counter- で始まるプロパティと .todo-count::after)は、次の節で説明します。文字の大きさや余白など、見た目だけの CSS は省いています。

css
.todo {
  list-style: none;
  padding: 0;
  counter-reset: done total;
}

.todo li {
  counter-increment: total;
}

.todo input:checked {
  counter-increment: done;
}

.todo input:checked + span {
  text-decoration: line-through;
  color: #9aa5ab;
}

.todo-count::after {
  content: counter(done) " / " counter(total) " 件完了";
}

取り消し線を付けているのは、.todo input:checked + span のルールです。:checked は、チェックの入ったチェックボックスに当てはまります。+(次兄弟結合子)は、同じ親の中で直後にある要素を選びます。合わせると「チェックの入ったチェックボックスの、すぐ後ろの span」になり、その文字に text-decoration: line-through で線を引いて、色を灰色にします。

検証環境で「牛乳を買う」の文字をクリックすると、チェックボックスに checked が入り、span の text-decoration-line は line-through、色は rgb(154, 165, 171)(#9aa5ab)になりました。スクリーンショットでも、文字の幅90pxいっぱいに横線が引かれていました。チェックしていない2行目は none のままです。もう一度クリックしてチェックを外すと、線は消えました。

文字のところをクリックしてもチェックが切り替わるのは、チェックボックスを <label> の中に入れているためです。MDN の <label> の説明のとおり、<input> を <label> の内側に入れると、for と id を書かなくても、ラベルをクリックしたときに入力欄をクリックしたのと同じことが起きます。

部品は普通のチェックボックスのままなので、キーボードでも使えます。ページを開いて Tab キーを1回押すと「牛乳を買う」のチェックボックスにフォーカスが移り、Space キーでチェックが入って、取り消し線が付きました。

ラベルの文字を3回クリックしたときの動きが、次の GIF です。

上の帯に「クリック 0回目/3回」と「チェック済み 0件」が出ている。マウスポインターが「牛乳を買う」の文字へ動いてクリックすると、「クリック 1回目/3回」と「チェック済み 1件」がピンクの地になり、「牛乳を買う」にチェックが入って、文字が灰色になり取り消し線が付く。リストの下の表示は「1 / 5 件完了」になる。2回目は「記事の下書きを書く」に線が付いて「2 / 5 件完了」に、3回目はもう一度「牛乳を買う」をクリックし、チェックと線が消えて「1 / 5 件完了」に戻る
ラベルの文字を3回クリックしたときの動き。3回目は、1回目にチェックした「牛乳を買う」をもう一度押してチェックを外している。上の帯とマウスポインターの絵は GIF 用に重ねたもの

終わった数を CSS カウンターで出す

「2 / 5 件完了」の表示は、CSS カウンターで作っています。ul の counter-reset: done total で、done と total の2つのカウンターを0から始めます。counter-increment は、当てはまった要素ごとにカウンターを1ずつ増やします。li で total を、チェックの入ったチェックボックスで done を増やし、最後に .todo-count::after の content で2つの値を文字にします。

検証環境で、件数の段落のスクリーンショットを、同じ文字を content に直接書いた見本と比べました。チェックした数に合わせて表示が変わっています。

操作 表示
何もチェックしていない 0 / 5 件完了
「ゴミを出す」をチェック 1 / 5 件完了
「洗濯物をたたむ」もチェック 2 / 5 件完了
「ゴミを出す」のチェックを外す 1 / 5 件完了

4つとも、見本と画素まで同じでした。Chromium のアクセシビリティツリーでも、段落の中の文字は "2"、" / "、"5"、" 件完了" のように、数字の入った形で出ていました。

件数の段落は、リストより後ろに置きます。同じ1件チェック中の状態で、段落を ul より前に移すと、表示は「0 / 0 件完了」になりました。CSS カウンターの説明のとおり、要素はカウンターの値を、親や前の兄弟の要素から受け取ります。リストより前にある段落には、数えた値がまだ渡ってきません。

文字をチェックボックスの前に置くなら :has() を使う

+ は「すぐ後ろ」の要素しか選べません。次のように、文字をチェックボックスの前に置いた HTML では、input:checked + span は効きません。

html
<li><label><span>牛乳を買う</span> <input type="checkbox"></label></li>

検証環境でも、チェックは入りましたが、span の text-decoration-line は none のままで、線は引かれませんでした。

このときは、:has() で「チェックの入ったチェックボックスを中に持つ li」を選び、li に線を付けます。

css
.todo li:has(input:checked) {
  text-decoration: line-through;
  color: #9aa5ab;
}

label を display: flex にしたページで試すと、文字が前にある HTML でも、チェックすると線が引かれ、文字が灰色になりました(label の display が線に関わることは、次の節で説明します)。:has() は、互換性データでは Chrome と Edge は 105、Firefox は 121、Safari は 15.4(iOS の Safari も 15.4)から使えます。:checked は Chrome 1、Firefox 1、Safari 3.1 から、+ は Chrome 1、Firefox 1、Safari 1 から使えます。

:has() で li に付けた線は、label が inline-flex だと出ない

li に付けた取り消し線は、中の要素の表示方法によっては文字に届きません。検証のページでは、チェックボックスと文字の高さをそろえるために、見た目の CSS で label を display: inline-flex にしています。この状態で上の :has() の CSS を使うと、li の text-decoration-line は line-through になっているのに、画面には線が出ませんでした。文字の色は灰色に変わるので、色だけ変わって線がない状態になります。

label の display 文字の色 線
inline-flex 灰色 出ない
flex 灰色 出る
inline 灰色 出る

CSS の仕様(CSS Text Decoration Module Level 3)の、線の装飾の節には、子孫に inline-block のような「atomic inline」の要素があると、その中身には文字の装飾が伝わらない、と書かれています。CSS Display の定義では、atomic inline は、行の中に並ぶ要素のうち、画像のような置換要素か、中に新しいレイアウトの文脈を作る要素です。inline-flex の label は、中で flex のレイアウトを作るので、これに当たります。そのため、li の線は label の中の文字まで届きません。label を flex にするか、線を付けるルールを span に書けば、線が出ます。最初に示した input:checked + span の書き方は、span そのものに線を付けるので、label が inline-flex でも線が出ていました。

線をふわっと出す

チェックした瞬間に線を出すのではなく、少しずつ濃くしたいときは、線を最初から引いておき、線の色を透明から灰色へ変えます。

css
.todo input + span {
  text-decoration: line-through;
  text-decoration-color: transparent;
  transition: text-decoration-color 0.3s, color 0.3s;
}

.todo input:checked + span {
  text-decoration-color: #9aa5ab;
  color: #9aa5ab;
}

チェック前の text-decoration-color は rgba(0, 0, 0, 0) で、スクリーンショットにも線は写りませんでした。チェックしてから約80msごとに読むと、rgba(154, 165, 171, 0.345)、0.804、0.97 と濃くなり、約310ms後に rgb(154, 165, 171) で止まりました。text-decoration-color は線の色を決めるプロパティで、transition を付けると、このように途中の色を通って変わります。

アクセシビリティツリーで完了を表すのは、チェックの状態だけ

2件をチェックしたあとのアクセシビリティツリーは、次のとおりでした(Playwright の ariaSnapshot() の出力から、最初の2つの listitem を抜き出しています)。

text
- list:
  - listitem:
    - checkbox "牛乳を買う"
    - text: 牛乳を買う
  - listitem:
    - checkbox "ゴミを出す" [checked]
    - text: ゴミを出す

完了した行の違いは、チェックボックスの [checked] だけです。文字の部分はどちらも text: ゴミを出す のような形で、取り消し線を付けたことはツリーに出ていません。この作り方では、終わったかどうかを支援技術に伝える情報は、チェックボックスの状態です。

チェックの状態は、ページを開き直すと消える

この CSS は、チェックの状態をどこにも保存しません。検証環境の Chromium で2つチェックしてから、再読み込み、別のページへ移って「戻る」、新しいタブで開く、新しいブラウザーコンテキスト(別のプロファイルで開いたのと同じ状態)で開く、の4通りを試すと、どれもすべてのチェックが外れていました。次に開いたときまで覚えておくには、チェックの状態をどこかに保存する仕組みを別に作る必要があります。

​Posted by 86