なぜ今さら「CSSカウンター」なのか?——olタグの呪縛から解き放たれる時
フロントエンド開発の現場で、リストを実装する際に「とりあえず `ol` を使っておけばいい」という判断をしていませんか?
もちろん、セマンティクス(意味論)の観点からは `ol` は正解です。しかし、デザインカンプを眺めていると、時折「これ、本当にHTMLの標準機能だけで実装できるのか?」と頭を抱えたくなるような複雑なナンバリングに出くわすはずです。
例えば、「入れ子構造の複雑なステップ表示」や「見出しごとにリセットされる連番」、あるいは「全く別のdiv要素で構成されたカードレイアウトに動的な番号を振りたい」といったケース。これらをJavaScriptでDOMを操作して番号を付与するのは、パフォーマンス面でも保守性の面でも、正直あまり賢い選択とは言えません。
そこで登場するのが、CSSの隠れた実力者 「CSSカウンター」 です。今回は、ブラウザのレンダリングエンジンが裏側でどう動いているのかという本質的な理解と、実務で即戦力となるテクニックを伝授します。
—
CSSカウンターの裏側:ブラウザはどう「カウント」しているのか
CSSカウンターは、ブラウザがレンダリングツリーを構築する過程で保持する「内部的な変数」のようなものです。具体的には以下の3つのプロパティを組み合わせて制御します。
1. `counter-reset`: カウンターを初期化(または特定の値にリセット)します。
2. `counter-increment`: カウンターの値を加算します。
3. `counter()` / `counters()`: 実際に表示させるための関数です。
ブラウザは、このプロパティを読み込むとメモリ上に専用のカウンターオブジェクトを生成します。興味深いのは、これが「DOMの構造」と密接に連動しているという点です。親子関係にある要素で `counter-reset` を行うと、その子要素からは新しいスコープのカウンターが有効になります。この「スコープの自動管理」こそが、CSSカウンターが強力な理由です。
—
【実践】実務で使える「ネストされたステップ表示」の実装
よくある「STEP 1-1, 1-2…」のような、階層構造を持つリストを `div` だけで実装する例を見てみましょう。
/ 親コンテナでカウンターを初期化 /
.step-container {
counter-reset: main-step; / ‘main-step’ という名前のカウンターをリセット /
}
/ 親ステップの定義 /
.step-item {
counter-increment: main-step; / 値を1ずつ増やす /
counter-reset: sub-step; / 子ステップのためにサブカウンターを初期化 /
margin-bottom: 20px;
}
/ 親ステップの番号表示 /
.step-item::before {
content: “STEP ” counter(main-step) “: “;
font-weight: bold;
}
/ 子ステップの定義 /
.sub-step-item {
counter-increment: sub-step; / 子カウンターを増やす /
margin-left: 20px;
}
/ 子ステップの番号表示(親の番号と連結) /
.sub-step-item::before {
content: counter(main-step) “-” counter(sub-step) ” “;
color: #666;
}
このコードのポイント
- セマンティクスの分離: HTML側は `div` や `article` で自由に構成できるため、デザイン崩れを恐れて `ol` の構造を無理やりCSSで上書きするような「ハック」が不要になります。
- メンテナンス性: リストの順序を入れ替えても、CSSが自動的に番号を再計算してくれます。JSで `innerHTML` を書き換えるようなコストは一切かかりません。
—
現場で役立つ「ちょっとしたコツ」
実務でCSSカウンターを使いこなす上で、以下の2点は必ず覚えておいてください。
1. `:before` 擬似要素との組み合わせは必須
カウンターは基本的に `content` プロパティ経由で出力します。つまり、擬似要素(`::before` や `::after`)を使える場所であればどこでも番号を振れます。画像アイコンの横に番号を配置したり、カードの右上にバッジとして番号を置くなど、デザインの自由度が飛躍的に上がります。
2. `display: none` との相性
ここが落とし穴です。ブラウザの仕様上、「レンダリングツリーから除外された要素(`display: none`)は、カウンターの対象外になる」というルールがあります。逆に `visibility: hidden` はツリーには残るため、カウンターは進みます。意図せぬ連番の飛びが発生した際は、要素の表示状態を疑ってみてください。
—
まとめ:CSSの「宣言的」な強みを活かす
CSSカウンターを使うことは、単にコードを短くするためではありません。「番号はデータではなく、表示上の装飾である」というCSSの本質的な役割に立ち返る行為です。
HTMLは情報の意味(セマンティクス)を記述し、CSSはそれをどう見せるか(プレゼンテーション)を担当する。この原則を崩さずに、複雑なナンバリングを実現できるCSSカウンターは、まさにフロントエンドエンジニアが身につけておくべき「スマートな引き出し」の一つです。
次の案件で「リスト番号が面倒だな」と感じたら、ぜひこの記事を思い出して実装に取り入れてみてください。コードの美しさが、そのままプロダクトの品質に直結するはずです。

コメント