Using CSS counters - draft

Author: @debiru_R

§ CSS カウンターの使用

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

カウンターには、カウントアップする通常のカウンターと、カウントダウンする逆行カウンターがあります。

カウンターは本質的には CSS が管理する変数であり、CSS カウンターだけでなく順序付きリストの番号も同様に管理されるものであるとCSS 仕様書には書かれています。カウンターは CSS のルールによって任意の値だけ増加・減少させることができ、名前付きカウンターを複数定義したり、順序付きリストで標準的に生成される list-item のカウンターを操作したりすることもできます。

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

ここでは、カウンターの基本的な使い方の説明と、各ブラウザにおける実装状況について説明します。

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

カウンターを使用するには、最初に counter-reset プロパティでカウンターを初期化する必要があります。

§ 通常のカウンター

§ 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>

counter-reset, counter-set, counter-increment の挙動を理解するために、まずはこれらを指定せずにカウンターの値を表示してみましょう。カウンター名には initial, inherit, unset, revert, none 以外であれば好きな名前が使えます。ここでは試しに num というカウンターを使います。

§ CSS-1

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

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

§ Result-1

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

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

次に、counter-increment を指定してみましょう。

§ CSS-2

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

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

§ Result-2

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

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

採番が div を飛び越えてしまっています。div の中で独立した採番となるように counter-reset を指定してみましょう。

§ CSS-3

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

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

§ Result-3

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

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

このように counter-reset, counter-increment, content を組み合わせて使うのが基本的なカウンターの使い方です。counter-reset の指定がある要素を起点に、そのカウンター名のスコープが作られます 。カウンターの通し番号を独立させたいスコープに対応する要素に counter-reset を指定するようにしましょう。

また、counter-increment の数値を指定することで、カウントアップ時の増量を変更することができます。

§ CSS-4

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

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

§ Result-4

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

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

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

§ CSS-5

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

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

§ Result-5

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

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

§ counter() と counters()

ネストされたHTMLに対して counter-reset を用いると、ネストされたカウンターを表現することができます。

§ 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-1

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

このような HTML と CSS は次のように表示されます。

§ Result-1

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

ここで、counter() の代わりに counters() を用いるとネストされたカウンターを表示することができます。次の例を見てください。

§ CSS-2

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

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

§ Result-2

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

順序付きリストの marker をネストされたカウンターなどに変更したい場合は、後述の 暗黙的な list-item カウンターも参考にしてください。

§ 逆行のカウンター

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

§ 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>

まずは、単純に counter-resetcounter-increment の数値を変更してカウントダウンの表現を試みる例を見てみましょう。

§ CSS-1

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

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

§ Result-1

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

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

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

§ CSS-2

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

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

§ Result-2

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

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

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

§ CSS-3

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

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

§ Result-3

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

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

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

counter(), counters() 関数は、それぞれ counter(<counter-name>, <counter-style>), counters(<counter-name>, <string>, <counter-style>) の引数を受け付けます。<counter-style>list-style-type と同じ値を受け付けます。

§ HTML

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

<counter-style>lower-roman を指定した例を見てみましょう。

§ CSS

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

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

§ Result

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

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

§ CSS カウンターの詳細な振る舞い

§ ネストされたカウンターとスコープ

counter-resetcounter-set の違いとして、counter-reset はネストされたカウンターを生成することが挙げられます。次の例を見てください。

§ 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: counters(num, "-") ". "; }

このような HTML と CSS は次のように表示されます。

§ Result

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

このように、カウンターは同じ名前のカウンターをネストする場合があります。ある要素上で新たなカウンターをインスタンス化するとき、親から同じ名前のカウンターを継承している場合には既存のカウンターの中に新しいカウンターがネストされて作成されます。上記の表示結果において 1-2. の次に 1-3. ではなく 2. が表示されるのはカウンターのスコープがネストごとに存在しているためです。counter-reset ではネストされたカウンターを生成できるため、上記のコードでは期待されるネストされた採番結果を得ることができます。

上記の例において、もし ol { counter-reset: num; }ol { counter-set: num; } と書いた場合には、次のように表示されます。

§ Result

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

§ フラットなカウンター

フラットな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 に対して次のように表示したいとします。

§ 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 を組み合わせて CSS を記述する必要があります。なぜならば、後述の仕様変更がされた Firefox 82 以降では counter-reset が新しいスコープを作ってしまうため、現在のスコープのカウンターを再度リセットすることができません。現在のスコープのカウンターをリセットするためには counter-set を使う必要があるのです。

§ 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-setcounter-reset と書いても期待する結果が得られていました。しかし、後述の誤ったHTMLに対する救済措置を実装する都合でこの振る舞いは変更されました。Firefox 82 からは上記の counter-setcounter-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)

§ 誤ったHTMLに対する採番

CSS の仕様では、順序付きリストに対する list-item の marker での採番はCSS カウンターによって計算されることになっています。ここでは分かりやすく独自のカウンターを使って説明しますが、デフォルトの list-item の marker も同様の結果となるべきです。次の例を見てください。

§ 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); }

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

§ Result

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

不正な HTML は様々な理由で存在しており、無視のできない状況にありました(なんと document.execCommand('indent') によって生成されるDOMも不正な HTML と同じ状況を生じさせます)。この採番を妥当な HTML の場合(ネストされたol要素が直前のli要素の子となるような場合)と同じ結果にするため Firefox 82 からは counter-reset の振る舞いが変更され、次のように表示されるようになりました。

§ Result

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

後述の暗黙的な list-item カウンターでも紹介しているように、li::marker に対する content プロパティを counter() ではなく counters() を用いて書き換えると、不正な HTML であってもネストされたli要素の marker は次のようにネストされたカウンターとして表示されます。

§ Result

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

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

順序付きリストでは marker に採番された値が表示されますが、この marker の値はカウンターとして操作することができます。

内部的には 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)

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

§ Result

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

独自のカウンターとデフォルトの marker の採番を同時に使いたい場合は counter-reset: my-counter list-item のように暗黙の list-item 名を明示することで、marker の採番を壊さずに独自のカウンターを使用することができます。

§ 使用例

§ セクションの明示

§ 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; }

§ 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 counters */
table { counter-reset: count; }
tbody tr { counter-increment: count; }
tbody td:nth-of-type(1)::before { content: counter(count); color: red; }

§ 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) "]"; }

§ 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); }

§ Result

§ ブラウザの実装

§ CSS counter scope/inheritance is compatible with HTML ordinals

順序付きリストの list-item marker の採番と、CSS カウンターによる採番が一致しているかどうかについてです。従来では誤ったHTMLに対する採番が list-item の marker とCSS カウンターで一致していませんでした。次の例を見てください。

§ HTML

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

<div>
  <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>
</div>

§ CSS

ol { counter-reset: list-item num; }
li { counter-increment: num; }
li::before { content: counter(num) ". "; }
li::before { color: red; }

div { counter-reset: num1 num2 num3; }
h1 { counter-increment: num1; counter-reset: num2 num3; }
h2 { counter-increment: num2; counter-reset: 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) ". "; }
h1, h2, h3 { margin: 0; font-size: 1em; font-weight: normal; }

このような HTML と CSS は次のように表示されます。上記の2個目の例の CSS では counter-set ではなく counter-reset を使っています。

§ Result

順序付きリストの表示結果ですが、従来はデフォルトの marker による採番とCSS カウンターによる ::before 疑似要素での採番が一致していませんでした。Firefox 82 ではこれを一致させるために counter-reset のスコープを厳密なものにする仕様が変更されました。

その副作用として、従来ではフラットな HTML に対して counter-reset のみを用いてネストされたカウンターを表現する方法が使えましたが、counter-reset が厳密なスコープを実現したことで2個目の例での採番が不自然なものになってしまいました。

詳細はフラットなカウンターを参照してください。

§ Does not apply implicit counter-reset: list-item

counter-reset を独自のカウンターのための記述でプロパティ値を上書きした場合に、暗黙的に counter-reset: 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: none; }

§ Result

Firefox 68 では counter-set の導入やCSS カウンターの内部実装が改められましたが、その中で counter-reset の暗黙的な適用が廃止されました。このため、counter-reset プロパティを指定した場合、list-item プロパティ値を明示しないと順序付きリストの採番が壊れてしまいます。従来は counter-reset: list-item が暗黙的に適用されるように振る舞っていました。

詳細は暗黙的な list-item カウンターを参照してください。

§ ブラウザーの互換性

Desktop Mobile
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet
counter-reset 2 12 1 8 9.2 3 1 18 25 10.1 1 1.0
counter-increment 2 12 1 8 9.2 3 1 18 25 10.1 1 1.0
counter() 1 12 1 8 9.2 3 1 18 4 10.1 1 1.0
counters() 1 12 1.5 8 10 3 1 18 4 10.1 1 1.0
@counter-style 91 91 33 No 77 No 91 91 33 64 No 16.0
counter-set 85 85 68 No 71 No 85 85 68 60 No 14.0
reversed() No No 96 No No No No No 96 No No No
CSS counter scope/inheritance is compatible with HTML ordinals No No 82 No No No No No 82 No No No
Does not apply implicit counter-reset: list-item No No 68 No No No No No 68 No No No