Using CSS counters - draft

Author: @debiru_R

§ CSS カウンターの使用

CSS カウンターでは、要素に対してカウンターを設定し、そのカウンターを表示することができます。例えば、ウェブページ内の見出し番号を自動的に振ったり、順序付きリストの番号を変更したり、特定のセレクターにマッチする要素のインデックス番号を表示したりするのに使用することができます。

カウンターを使うには、counter-reset, counter-increment, counter-set プロパティおよび content プロパティ値としての counter(), counters() 関数を用います。逆行カウンターの場合は counter-reset プロパティ値としての reversed() 関数を用いることもできます。

実際に HTML と CSS の記述を見ながら、カウンターの使い方を見ていきましょう。

§基礎編:カウンターの基本的な使い方

カウンターを使うには、カウンターの初期化、カウンターの増減、カウンターの表示の 3 つの手順を組み合わせます。一つずつ見ていきましょう。

§カウンターの表示

カウンターの状態を確認するために、先に表示から試してみましょう。

§HTML

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
  <p>(4)</p>
</div>

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
</div>

§CSS

p::before { content: counter(num) ". "; }

content プロパティで counter() 関数を使っています。この記述によりカウンター名 num のカウンターを表示できます。カウンター名には initial, inherit, unset, revert, none 以外であれば好きな名前が使えます。ここでは試しに num としています。

この CSS を適用すると、次のように表示されます。

§Result

0. (1)
0. (2)
0. (3)
0. (4)

0. (1)
0. (2)
0. (3)

§カウンターの増減

num カウンターを 1 ずつ増加させてみましょう。

§CSS

p { counter-increment: num 1; }
p::before { content: counter(num) ". "; }

counter-increment プロパティでカウンター名に続けて数値を指定します。なお、数値を省略すると 1 を指定したものとして扱われます。下記の記述は、上記と同じ結果になります。

p { counter-increment: num; }
p::before { content: counter(num) ". "; }

この CSS を適用すると、次のように表示されます。

§Result

1. (1)
2. (2)
3. (3)
4. (4)

5. (1)
6. (2)
7. (3)

§カウンターの初期化

前述の CSS だけでは div を飛び越えてカウンターが増減してしまっていました。こうしたことを防ぐため、カウンターを使いたいスコープでは必ず counter-reset プロパティで初期化を行います。

div { counter-reset: num; }
p { counter-increment: num; }
p::before { content: counter(num) ". "; }

§Result

1. (1)
2. (2)
3. (3)
4. (4)

1. (1)
2. (2)
3. (3)

このように counter-reset, counter-increment, content プロパティを組み合わせて使うのが基本的なカウンターの使い方です。counter-reset の指定がある要素を起点に、そのカウンター名のスコープが作られます。

§counter() と counters()

次のように HTML がネストしているような場合には content プロパティの counters() 関数が便利です。

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

ol { counter-reset: num; }
li { counter-increment: num; }
li::marker { content: counter(num) ". "; }

まずは counter() 関数を使ってみます。このような HTML と CSS は次のように表示されます。

§Result

1. (1)
    1. (1-1)
    2. (1-2)
2. (2)
    1. (2-1)
    2. (2-2)

§CSS

ol { counter-reset: num; }
li { counter-increment: num; }
li::marker { content: counters(num, "-") ". "; }

次に、counter() の代わりに counters() を使ってみます。この CSS を適用すると、次のように表示されます。

§Result

1. (1)
    1-1. (1-1)
    1-2. (1-2)
2. (2)
    2-1. (2-1)
    2-2. (2-2)

ネストしたHTMLに対して counter-reset を指定することで、自動的に階層的なカウンターが作られます。階層の末尾だけを表示したい場合は counter() 関数を、階層的なカウンターをそのまま表示したい場合は counters() 関数を使うことで、期待する結果を得ることができます。

§応用編:カウンターの詳細な振る舞い

§3 ずつカウントアップする

§HTML

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
  <p>(4)</p>
</div>

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
</div>

§CSS

div { counter-reset: num; }
p { counter-increment: num 3; }
p::before { content: counter(num) ". "; }

counter-increment の数値として 3 を指定しました。この CSS を適用すると、次のように表示されます。

§Result

3. (1)
6. (2)
9. (3)
12. (4)

3. (1)
6. (2)
9. (3)

このとき、counter-set で最初の値を変更することで、1 から始まり 3 ずつ増えるカウンターを表現できます。

§CSS

div { counter-reset: num; }
p { counter-increment: num 3; }
p:first-child { counter-set: num 1; }
p::before { content: counter(num) ". "; }

この CSS を適用すると、次のように表示されます。

§Result

1. (1)
4. (2)
7. (3)
10. (4)

1. (1)
4. (2)
7. (3)

§逆行カウンター

逆行のカウンターはカウントダウンを表現するために使います。

前述の HTML に対して、counter-resetcounter-increment の数値を変更してカウントダウンの表現を試みる例を見てみましょう。

§CSS

div { counter-reset: num 5; }
p { counter-increment: num -1; }
p::before { content: counter(num) ". "; }

この CSS を適用すると、次のように表示されます。

§Result

4. (1)
3. (2)
2. (3)
1. (4)

4. (1)
3. (2)
2. (3)

カウントダウンは表現できていますが、スコープごとに要素数が異なると採番の初期値がずれてしまいます。このような場合には reversed() 関数が便利です。

§CSS

div { counter-reset: reversed(num); }
p { counter-increment: num -1; }
p::before { content: counter(num) ". "; }

この CSS を適用すると、次のように表示されます。

§Result

4. (1)
3. (2)
2. (3)
1. (4)

3. (1)
2. (2)
1. (3)

カウントアップするカウンターと同様に counter-increment の数値を指定することで、カウントダウン時の減量を変更することができます。また、counter-set で最後の値を変更することで、1 から始まり 3 ずつ増えるカウンターを逆行するカウンターが表現できます。

§CSS

div { counter-reset: reversed(num); }
p { counter-increment: num -3; }
p:last-child { counter-set: num 1; }
p::before { content: counter(num) ". "; }

この CSS を適用すると、次のように表示されます。

§Result

10. (1)
7. (2)
4. (3)
1. (4)

7. (1)
4. (2)
1. (3)

§カウンタースタイルの変更

counter(), counters() 関数は、それぞれ次の引数を受け付けます。

  • counter(<counter-name>, <counter-style>)
  • counters(<counter-name>, <separator>, <counter-style>)

<counter-style>list-style-type と同じ値を受け付けます。

§HTML

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
  <p>(4)</p>
</div>

§CSS

div { counter-reset: num; }
p { counter-increment: num; }
p::before { content: counter(num, lower-roman) ". "; }

<counter-style>lower-roman を指定した例を見てみましょう。この CSS を適用すると、次のように表示されます。

§Result

i. (1)
ii. (2)
iii. (3)
iv. (4)

カウンタースタイルでは規定のものだけでなく、@counter-style を用いて独自の表示を行うことができます。

§暗黙的な list-item カウンター

順序付きリストでは marker に採番された値が表示されますが、この marker の値は CSS カウンターとして操作することができます。特別なカウンター名 list-item に対して設定を行います。

内部的には、次のようなスタイルが暗黙的に指定されているように振る舞います。

ol, ul, menu { counter-reset: list-item; }
li { counter-increment: list-item; }
li::marker { content: counter(list-item) ". "; }

このため、marker の採番を 1 以外の数値でカウントアップしたり、階層的なカウンターを表示させたりすることができます。

なお、counter-reset: reversed(list-item) が指定された場合には、暗黙的に li { counter-increment: list-item -1; } が指定されているように振る舞います。

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

li::marker { content: counters(list-item, "-") ". "; }

この例ではネストされた順序付きリストの HTML に対して、li::marker に対する content プロパティを counter() ではなく counters() を用いて書き換えるだけで階層的なカウンターを表示することができます。

§Result

1. (1)
    1-1. (1-1)
    1-2. (1-2)
2. (2)
    2-1. (2-1)
    2-2. (2-2)

§フラットな HTML に対する階層的なカウンター

フラットな HTML に対して階層的な採番を行いたい場合があります。次の例を見てください。

§HTML

<h1>(1)</h1>
<h2>(1-1)</h2>
<h2>(1-2)</h2>
<h3>(1-2-1)</h3>
<h1>(2)</h1>
<h2>(2-1)</h2>
<h2>(2-2)</h2>
<h3>(2-2-1)</h3>

この HTML に対して次のように表示したいとします。

§Expected Result

1. (1)
1-1. (1-1)
1-2. (1-2)
1-2-1. (1-2-1)
2. (2)
2-1. (2-1)
2-2. (2-2)
2-2-1. (2-2-1)

これを実現させるためには、複数のカウンターを組み合わせて擬似的に階層的なカウンターを表現する必要があります。

§CSS

body { counter-reset: num1 num2 num3; }
h1 { counter-increment: num1; counter-set: num2 num3; }
h2 { counter-increment: num2; counter-set: num3; }
h3 { counter-increment: num3; }
h1::before { content: counter(num1) ". "; }
h2::before { content: counter(num1) "-" counter(num2) ". "; }
h3::before { content: counter(num1) "-" counter(num2) "-" counter(num3) ". "; }

counter-set を用いて、例えば 2 回目の h1 が現れたら、1 回目の h1 の配下としてインクリメントされていた num2, num3 のカウンターを 0 に戻すということを行っています。

なお、このケースでは、従来 counter-reset が使われていましたが、counter-set プロパティの導入に伴い counter-reset の仕様が変更されたことで、現在では counter-set を使わなければ期待する結果が得られないブラウザーがあります。詳細については厳密なスコープの副作用を参照してください。

ただし、counter-set をサポートしていないブラウザーがあるかもしれません。counter-set をサポートしていないブラウザーに対しては、従来のように counter-reset で下位レベルの見出しのカウンターを 0 に戻す必要があります。

§実際の使用例

§セクションの明示

§HTML

<div>
  <h1>Down the Rabbit-Hole</h1>
  <h1>Pool of Tears</h1>
  <h1>A Caucus-race and a Long Tale</h1>
</div>

§CSS

div { counter-reset: section; }
h1 { counter-increment: section; }
h1::before { content: "Section " counter(section) ": "; }
h1 { font-size: 1em; }

§Actual Result

§レンダリングされた要素のカウント

§HTML

<div>
  <input id="item-1" type="checkbox" checked /><label for="item-1">item-1</label>
  <input id="item-2" type="checkbox" checked /><label for="item-2">item-2</label>
  <input id="item-3" type="checkbox" checked /><label for="item-3">item-3</label>
  <input id="item-4" type="checkbox" checked /><label for="item-4">item-4</label>
  <input id="item-5" type="checkbox" checked /><label for="item-5">item-5</label>
  <table>
    <thead>
      <tr><th>count</th><th>index</th><th>value</th></tr>
    </thead>
    <tbody>
      <tr class="item-1"><td></td><td>1</td><td>Down the Rabbit-Hole</td></tr>
      <tr class="item-2"><td></td><td>2</td><td>Pool of Tears</td></tr>
      <tr class="item-3"><td></td><td>3</td><td>A Caucus-race and a Long Tale</td></tr>
      <tr class="item-4"><td></td><td>4</td><td>The Rabbit sends in a Little Bill</td></tr>
      <tr class="item-5"><td></td><td>5</td><td>Advice from a Caterpillar</td></tr>
    </tbody>
  </table>
</div>

§CSS

/* table border */
table { margin: 20px; border-collapse: collapse; }
th, td { padding: 4px 8px; border: 1px solid #999; text-align: center; }

/* filtering by input[type="checkbox"] */
tbody tr { display: none; }
#item-1:checked ~ table .item-1 { display: table-row; }
#item-2:checked ~ table .item-2 { display: table-row; }
#item-3:checked ~ table .item-3 { display: table-row; }
#item-4:checked ~ table .item-4 { display: table-row; }
#item-5:checked ~ table .item-5 { display: table-row; }

/* CSS counter */
table { counter-reset: count; }
tbody tr { counter-increment: count; }
tbody td:nth-of-type(1)::before { content: counter(count); color: red; }

§Actual Result

§内容が空のリンクを表示する

§HTML

<p>See <a href="https://www.mozilla.org/"></a></p>
<p>If you want to know more about us, please refer to <a href="https://developer.mozilla.org/en-US/docs/MDN/About">About MDN Web Docs</a></p>
<p>See also <a href="https://developer.mozilla.org/"></a></p>

§CSS

:root { counter-reset: link; }
a[href] { counter-increment: link; }
a[href]:empty::before { content: "[" counter(link) "]"; }

§Actual Result

§list-item カウンターを逆行させる

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

ol { counter-reset: reversed(list-item); }

§Actual Result

§CSS カウンターに関する注意点

§順序付きリストの採番と CSS カウンターの関係

従来は順序付きリストの採番は CSS カウンターとは別で独自に実装されていましたが、counter-set の導入に伴い CSS カウンターの仕様が改められた結果、順序付きリストの採番も CSS カウンターと同様に実装されることになりました。暗黙的な list-item カウンターを操作できるのは、この改められた仕様に追従しているブラウザーに限られます。

§不正な HTML に対する採番

§HTML

<ol>
  <li>(1)</li>
  <li>(2)</li>
  <ol>
    <li>(2-1)</li>
  </ol>
  <li>(3)</li>
  <li>(4)</li>
</ol>

§CSS

ol { counter-reset: num; }
li { counter-increment: num; }
li::marker { content: counter(num); }

ここでは分かりやすく CSS カウンターを使って説明しますが、デフォルトの list-item の採番も同様の結果となることが CSS の仕様では想定されています。

<ol> 要素直下に <ol> 要素が出現しています。これは不正な HTML ですが、従来は上記の HTML と CSS では次のように表示されました。

§Result

1. (1)
2. (2)
    3. (2-1)
2. (3)
3. (4)

不正な HTML は様々な理由で存在しており、無視のできない状況にありました(なんと document.execCommand('indent') によって生成される DOM も不正な HTML と同じ状況を生じさせます)。この採番を妥当な HTML の場合と同じ結果にするため、一部の新しいブラウザーでは「厳密なスコープ」を実装することによって counter-reset の振る舞いが変更されました。厳密なスコープが実装されたブラウザーでは次のように表示されます。

§Result

1. (1)
2. (2)
    1. (2-1)
3. (3)
4. (4)

§厳密なスコープの副作用

§HTML

<h1>(1)</h1>
<h2>(1-1)</h2>
<h2>(1-2)</h2>
<h3>(1-2-1)</h3>
<h1>(2)</h1>
<h2>(2-1)</h2>
<h2>(2-2)</h2>
<h3>(2-2-1)</h3>

§CSS

body { counter-reset: num1 num2 num3; }
h1 { counter-increment: num1; counter-set: num2 num3; }
h2 { counter-increment: num2; counter-set: num3; }
h3 { counter-increment: num3; }
h1::before { content: counter(num1) ". "; }
h2::before { content: counter(num1) "-" counter(num2) ". "; }
h3::before { content: counter(num1) "-" counter(num2) "-" counter(num3) ". "; }

フラットな HTML に対して階層的な採番を行いたい場合は、上記のような CSS で実現することができました。

§Result

1. (1)
1-1. (1-1)
1-2. (1-2)
1-2-1. (1-2-1)
2. (2)
2-1. (2-1)
2-2. (2-2)
2-2-1. (2-2-1)

ここで、厳密なスコープを実装したブラウザーの場合、counter-set の代わりに counter-reset を使うと、次のように表示されてしまいます。

§Result

1. (1)
1-1. (1-1)
1-2. (1-2)
1-2-1. (1-2-1)
2. (2)
2-3. (2-1)
2-4. (2-2)
2-4-2. (2-2-1)

counter-set が導入される前は counter-reset を用いてこのケースが実装されていたことに注意が必要です。厳密なスコープが実装されている Firefox 82 以降ではこの問題の影響を受けます。

§暗黙的な counter-reset: list-item の非適用性

暗黙的な list-item カウンターでは、以下の CSS が暗黙的に適用されると説明しました。

ol, ul, menu { counter-reset: list-item; }
li { counter-increment: list-item; }
li::marker { content: counter(list-item) ". "; }

/* counter-reset: reversed(list-item) が指定された場合 */
li { counter-increment: list-item -1; }

CSS の仕様では counter-increment プロパティを(独自の CSS カウンターのための記述で)上書きした場合でも、内部的には counter-increment: list-item が適用されているものとして振る舞うこととされています。

一方で、counter-reset プロパティについてはそのような取り決めがありません。従来は counter-reset が(独自の CSS カウンターのための記述で)上書きされた場合でも暗黙的に適用されているように振る舞いましたが、一部のブラウザーでは counter-reset: list-item の暗黙的な適用がされず、li::marker に対するデフォルトの採番が壊れてしまうことがあります。

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

ol { counter-reset: none; }

counter-resetlist-item に対する暗黙的な適用が行われないブラウザーの場合は、次のように表示されます。

§Result

1. (1)
    2. (1-1)
    3. (1-2)
4. (2)
    5. (2-1)
    6. (2-2)

§Expected Result

本来、期待される表示結果は次の通りです。

1. (1)
    1. (1-1)
    2. (1-2)
2. (2)
    1. (2-1)
    2. (2-2)

期待する表示結果を維持するためには、明示的に list-item に対する counter-reset を記述する必要があります。

§CSS

/* none の代わり */
ol { counter-reset: list-item; }

/* 独自の CSS カウンターと list-item を併用したい場合 */
ol { counter-reset: num list-item; }

暗黙的な counter-reset: list-item の適用を行わない Firefox 68 以降ではこの問題の影響を受けます。

§関連情報