CSSだけで、目次のいま読んでいる節を目立たせる方法

​​CSS​HTML,Playwright,アクセシビリティ

目次のリスト(<ol>)に scroll-target-group: auto を1行書くと、いま読んでいる節へのリンクが :target-current で選べるようになります。どのリンクを「いまの位置」とするかはブラウザーが決め、スクロールに合わせて切り替えます。スクロール位置を調べる JavaScript は要りません。

左に目次、右に本文があるページを下へスクロールしていく。本文の節の上端の水色の線が、画面の中央の点線を越えるたびに、目次でピンクになる項目が「はじめの節」から「よくある質問」まで1つずつ下へ移る
ページを下へスクロールしたときの、目次の強調の動き。上部の scrollY の表示と、強調が切り替わった瞬間のピンクの表示、画面の中央の点線、本文の行に見立てた灰色の帯は、録画のために足したもの。画面の大きさ(640×480)と上の余白が本文の検証用のページと違うため、scrollY の値は本文の表と一致しない
補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、ヘッドレスの Chromium 153 です。検証用のページ(画面の大きさ 800×600)を Playwright で開いてスクロールやクリックをし、どのリンクに :target-current が当たっているかを element.matches() で調べました。Firefox や Safari での動き、スクリーンリーダーでの読み上げ、指やトラックパッドでのスクロールは試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

目次のリストに1行書き、:target-current に色を付ける

目次は、ページ内の節へのリンクを並べたふつうのリストです。リンクの href の # のあとに、本文の節の id を書きます。

html
<div class="layout">
<nav class="toc" aria-label="目次">
  <ol>
    <li><a href="#intro">はじめの節</a></li>
    <li><a href="#install">インストール</a></li>
    <li><a href="#usage">使い方</a></li>
    <li><a href="#options">オプション</a></li>
    <li><a href="#faq">よくある質問</a></li>
  </ol>
</nav>
<main>
  <h1>記事のタイトル</h1>
  <section id="intro"><h2>はじめの節</h2>…</section>
  <section id="install"><h2>インストール</h2>…</section>
  <!-- 残りの節も同じ -->
</main>
</div>

CSS で、目次の <ol> に scroll-target-group: auto を書き、:target-current に当たるリンクに色を付けます。検証用のページでは、グリッドで目次を左に置き、position: sticky で画面に残るようにしました。

css
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 24px;
}
.toc {
  position: sticky;
  top: 16px;
  align-self: start;
}
.toc ol {
  scroll-target-group: auto;
  list-style: none;
  margin: 0;
  padding: 0 0 0 12px;
}
.toc a {
  display: block;
  padding: 6px 10px;
  color: #1f2a30;
  text-decoration: none;
  border-left: 3px solid transparent;
}
.toc a:target-current {
  color: #d85878;
  font-weight: bold;
  border-left-color: #d85878;
  background: #fdeef2;
}

MDN の scroll-target-group のページによると、auto を書いた要素は「スクロールマーカーグループコンテナー」になり、その中にある # 付きの <a> が自動でスクロールマーカーになります。そのうち、リンク先がいま表示されているものが :target-current に当たります。

検証環境で scrollY を 0 から最後まで 1px ずつ動かして調べたところ、:target-current に当たるリンクは、どの位置でも必ず1つでした。ページの一番上(scrollY が 0)でも「はじめの節」が当たっていて、何も強調されない状態にはなりませんでした。scroll-target-group の行だけを消したページでは、同じように調べても1つも当たりませんでした。リンク先の id がページにない項目を1つ足したページでも同じように調べたところ、その項目は強調されず、ほかの項目が切り替わる位置も足す前と同じでした。

強調が切り替わるのは、節の先頭が画面の中央を過ぎたとき

検証用のページは、高さ 200px のタイトルのあとに、高さ 700px の節を5つ並べたものです。scrollY を 0 から 1px ずつ動かし、強調が切り替わる位置を調べました。画面の高さは 600px です。

強調される項目 節の上端の位置 切り替わった scrollY
はじめの節 200 0(最初から)
インストール 900 601
使い方 1600 1301
オプション 2300 2001
よくある質問 3000 2701

scrollY が 601 のとき、「インストール」の節の上端は画面の上から 299px の位置にあります。画面の高さ 600px の半分より少し上です。つまり、次の節の先頭が画面の中央を越えて上へ来たところで、強調が次の項目へ移りました。冒頭の GIF でも、節の上端の水色の線が点線(画面の中央)を越えると、目次のピンクの項目が1つ下へ移っています。

この選び方は、仕様(CSS Overflow Module Level 5 の、アクティブなスクロールマーカーの計算)に例として書かれている手順に沿っています。仕様の例では、先頭が画面の上端まで来た(または上端を越えた)節のうち最後のものを選ぶほか、直前の節の先頭が画面の半分以上上へ出ていれば、画面の上半分に入ってきた節を選びます。

ページの最後の短い節も、下端までスクロールすると強調される

最後のほうの節が短いと、ページの下端までスクロールしても、その節の先頭が画面の中央まで上がってきません。そこで、4つ目と5つ目の節の高さを 150px にしたページで調べました。このページのスクロールの最大値は 2000 で、4つ目の節の上端(2300)にも5つ目の節の上端(2450)にも届きません。

強調される項目 切り替わった scrollY
使い方 1301
オプション 1790
よくある質問 1990

スクロールの終わり近くの 200px ほどの間に、「オプション」と「よくある質問」が順に強調されました。下端まで行けば、最後の項目が強調されます。仕様の例の手順にも、端まで届かない節の位置を端の近くに割り振り直す考え方が書かれています。ただし、仕様の例で割り振り直す範囲は、このページでは 75px にあたり、検証で見た約 200px とは一致しませんでした。

固定ヘッダーがあるときは、scroll-padding-top で切り替わりを早める

ページの上に固定のヘッダーがあると、本文の上のほうはヘッダーに隠れます。検証用のページに高さ 80px の固定ヘッダーを置き、その分だけ本文を下げて調べました(節の上端は 980、1680、2380、3080 になります)。

ヘッダーを置いただけでは、強調が切り替わった scrollY は 681、1381、2081、2781 でした。どれも節の上端が画面の上から 299px の位置に来たときで、ヘッダーがないときと同じく画面の中央が境目です。ヘッダーの高さの分は、切り替わりの位置に反映されません。

ルート要素に scroll-padding-top でヘッダーの高さを書くと、切り替わりの位置が変わりました。

css
html {
  scroll-padding-top: 80px;
}

固定ヘッダーと scroll-padding-top: 80px の両方を書いたページでは、切り替わった scrollY は 601、1301、2001、2701 で、ヘッダーだけのときより、どれもちょうど 80px 手前でした。節の先頭が上から 379px、つまりヘッダーの下端から 299px の位置に来たときに切り替わります。固定ヘッダーのないページに scroll-padding-top: 80px だけを書いたときも、切り替わりは 521、1221、1921、2621 と、書かないときより 80px 手前になりました。

リンクを押したときは、スクロールが終わる前に強調が移る

目次の「オプション」を押すと、ページは「オプション」の節へ移り、URL の末尾は #options になりました。強調も「オプション」に移ります。

html に scroll-behavior: smooth を書いて、スクロールが動いて見えるようにしたページでも試しました。押してから約 156ms 後、まだ scrollY が 380 のところで、強調はもう「オプション」になっていました。ふつうにスクロールしたときなら「はじめの節」が強調される位置です。そのあとも約 150ms おきに調べたところ、最後に調べた約 780ms 後(scrollY が 2299)まで、強調はずっと「オプション」のままでした。仕様には、スクロールが最終的に止まる位置をもとに強調する項目を決め、項目がすぐ切り替わるようにすると書かれています。

リンクを押したあとにホイールで上へ戻ると、URL の末尾は #options のままでしたが、強調は「使い方」「インストール」「はじめの節」と、スクロールした位置に合わせて戻りました。

入れ子の目次では、外側のリストだけに書く

小見出しの項目を、内側の <ol> で入れ子にした目次も試しました。

html
<li><a href="#install">インストール</a>
  <ol>
    <li><a href="#install-npm">npm で入れる</a></li>
    <li><a href="#install-cdn">CDN から読む</a></li>
  </ol>
</li>

ここで .toc ol { scroll-target-group: auto; } と書くと、内側の <ol> にも同じ指定が当たります。すると、目次の中に強調の単位が2つでき、項目が2つ同時に強調されました。ページの一番上でも「はじめの節」と「npm で入れる」の2つが強調され、「使い方」まで進んでも、内側では「CDN から読む」が強調されたまま残りました。仕様では、マーカーは、いちばん近い祖先のスクロールマーカーグループコンテナーのグループに入ることになっています。内側の <ol> にもグループができたので、小見出しのリンクは外側のグループから外れ、内側だけで1つずつ強調されたわけです。

外側のリストだけに書くには、最初の CSS の .toc ol の規則を、子セレクターの .toc > ol に書き換えます。検証用のページでは、次のように規則ごと書き換えました。

css
.toc > ol {
  scroll-target-group: auto;
  list-style: none;
  margin: 0;
  padding: 0 0 0 12px;
}
.toc ol ol {
  padding-left: 16px;
}

こうすると、強調される項目はいつも1つになりました。切り替わった scrollY は次のとおりです。

強調される項目 節の上端の位置 切り替わった scrollY
インストール 900 601
npm で入れる 961 931
CDN から読む 1211 1087
使い方 1600 1406

入れ子にしたページでは、小見出しの節と、その次の「使い方」の節で、先頭が画面の中央より上のほうに来てから切り替わりました。「npm で入れる」は上から 30px、「使い方」は上から 194px の位置まで来たときです。仕様の例の手順で計算すると、「npm で入れる」は 961、「使い方」は 1512 で切り替わることになり、観測した値とは一致しませんでした。

読んだ項目とまだの項目は、:target-before と :target-after で分けられる

強調している項目より前の項目は :target-before、後の項目は :target-after で選べます。検証環境で「使い方」を強調している位置にスクロールしたところ、:target-before には「はじめの節」と「インストール」が、:target-after には「オプション」と「よくある質問」が当たりました。

たとえば、読み終えた節の項目を薄くするなら、次のように書きます。

css
.toc a:target-before {
  color: #9aa5ab;
}

この2つの擬似クラスに対応したのは、:target-current より後のバージョンです(最後の節の表を参照)。

変わるのは見た目だけで、読み上げ用の情報は付かない

:target-current で変わるのは CSS の見た目だけです。検証環境で、Chrome DevTools Protocol からアクセシビリティツリーを取り出したところ、目次のリンクはどれも、強調されているかどうかに関係なく、ただの link でした。いまの項目を示す状態は付いておらず、リンクに aria-current 属性も付いていませんでした。

キーボードでの動きも、ふつうのリンクと同じです。Tab キーを押すと、強調している項目に関係なく、目次のリンクを上から順に1つずつ移りました。MDN の scroll-target-group のページにも、scroll-target-group で作ったマーカーには特別なナビゲーションの動きがなく、scroll-marker-group で作るマーカーのような tablist と tab の扱いや矢印キーでの移動は付かない、と書かれています。

Firefox と Safari は、まだ対応していない

互換性データでの対応状況は次のとおりです。MDN では、どれもまだ実験的な機能とされています。

ブラウザー scroll-target-group :target-current :target-before・:target-after
Chrome・Edge(Android の Chrome を含む) 140 から 135 から 142 から
Opera 124 から 120 から 126 から
Samsung Internet 30.0 から 29.0 から 30.0 から
Firefox(Android を含む) 対応していない 対応していない 対応していない
Safari(iOS を含む) 対応していない 対応していない 対応していない

:target-current は 135 から対応していますが、MDN の :target-current のページによると、当たるのは ::scroll-marker 擬似要素と、scroll-target-group でスクロールマーカーになった要素だけです。そのため、目次のリンクで使えるのは scroll-target-group に対応したバージョンからです。

検証環境の Chromium 153 では、CSS.supports('scroll-target-group', 'auto') と CSS.supports('selector(:target-current)')、:target-before・:target-after を同じように調べた結果が、どれも true でした。

​Posted by 86