現場で絶対にやらかす「:first-of-type」の誤解。ブラウザの評価ロジックから学ぶ、CSSセレクタの正しい付き合い方
フロントエンドの現場を率いるチーフアーキテクトの立場から、今日もコードレビューでよく見かける「あの問題」について話をしよう。
マークアップを進める中で、誰もが一度は「特定の要素の最初のターゲットにだけスタイルを当てたい」という場面に遭遇するはずだ。そして、多くのメンバーが「お、`:first-of-type`を使えば一発だな」とキーボードを叩く。
しかし、ブラウザを開いてみると、意図した要素にスタイルが当たっていなかったり、あるいは関係のない別の要素にまでスタイルが適用されてしまって、「えっ、なんで!?」と頭を抱える。
今回は、CSS仕様の裏側にあるブラウザの処理ロジックを解き明かしながら、`:first-of-type`の真実と、現場で二度とバグを生まないための実践的なテクニックを伝授する。
—
1. そもそも `:first-child` と `:first-of-type` は何が違うのか?
まずは基本の整理から始めよう。ここを曖昧にしたまま「なんとなく」使っていると、複雑なHTML構造になった瞬間に破綻する。
「親要素から見て、最初の子要素」である場合のみマッチする。
「親要素から見て、その要素と同じタグ名(要素型)の中で最初の要素」にマッチする。
言葉で書くとシンプルだが、実例を見るとその挙動の差が明確になる。
お知らせ
最初の段落テキスト
二番目の段落テキスト
このHTMLに対して、以下のCSSを当ててみよう。
/ パターンA::first-child /
p:first-child {
color: red; / 反映されない! /
}
/ パターンB::first-of-type /
p:first-of-type {
color: blue; / ②の「最初の段落テキスト」が青になる! /
}
なぜパターンAは赤くならないのか?
ブラウザは `p:first-child` を評価するとき、「まずそれが `p` タグであり、かつ『親要素の1番目の子ども』であるか」を見る。このHTMLにおける最初の子ども(①)は `span` タグだ。したがって、`p:first-child` は「該当なし」となり、何も染まらない。
なぜパターンBは青くなるのか?
一方、`p:first-of-type` は、「親要素内のすべての `p` タグ(要素型)の中で、最初の `p` タグ」を探す。①の `span` は無視され、`p` タグの中で一番最初に出現する ② がターゲットになる。
ここまでは「基本」だ。中級エンジニアの君なら、ここまでは理解できているかもしれない。
では、次に進もう。現場のエンジニアが最も頻繁に踏み抜く「大いなる罠」の話だ。
—
2. 【超重要】なぜ `.card:first-of-type` はあなたの意図通りに動かないのか?
現場のコードレビューで私が最も多く却下するのが、以下のようなコードだ。
/ 意図:.cardクラスを持つ最初の要素を赤くしたい /
.card:first-of-type {
border-color: red; / 結果:何も起きない、もしくは意図しない挙動になる /
}
「`.card` の中で最初の要素に当てるんだから、これでいいでしょ?」と思うかもしれない。しかし、これは動かない。
なぜか?
ブラウザは、このセレクタを「`.card` クラスを持つ要素の中で最初のもの」とは解釈しないからだ。
ブラウザの裏側の評価ロジック:CSSは「右から左」に解析される
ブラウザのレンダリングエンジン(BlinkやWebkitなど)がCSSセレクタを評価する順序は、我々人間が読む方向とは逆、つまり 「右から左(Right to Left)」 である。
`.card:first-of-type` というセレクタを、ブラウザは以下のようなステップで処理している。
1. まず右側(`:first-of-type`)を見る。
ブラウザは「親要素の中で、そのタグ名(要素型)において最初の要素」をすべてピックアップする。
※ここが最大のポイントだ。`:first-of-type` の「type」とは、クラス名のことではなく、`div` や `p`、`span` といったHTMLタグの要素型(Element Type)を指す。
2. 上記のHTMLにおいて、`div` タグの中で一番最初に出現するのは `
` だ。つまり、この時点でブラウザが「`div` の `first-of-type`」として認識するのは、
広告バナー(`ad-banner`)になる。
3.
次に左側(`.card`)を見る。
ブラウザは、先ほど選んだ「最初の `div`(広告バナー)」が、`.card` というクラスを持っているかを検証する。
4. 広告バナーは `.card` クラスを持っていない。
5.
結果:マッチング失敗。スタイルはどこにも適用されない。
> チーフアーキテクトの補足:
> もし、仮に広告バナーを消去して、先頭が `
` になれば、偶然にもスタイルは適用される。しかし、それは「`.card` の最初」を認識したからではなく、「`div` の最初」がたまたま `.card` を持っていたからに過ぎない。この「動いたり動かなかったりする不安定さ」こそが、CSSのマークアップを壊す温床なのだ。
—
3. 現場で今すぐ使える!実践的な解決パターンとコード例
この罠を回避し、実務で安全かつクリーンに最初の要素をハンドリングするためのパターンを紹介しよう。
エディタにそのまま貼り付けて、ブラウザでの挙動を確認してみてほしい。
パターンA:【最適解】そもそもタグ型を統一して `:first-of-type` を正しく活かす
構造的に、異なるタグを混ぜずに「同じコンポーネントは同じタグのリスト」としてマークアップするのが最も美しい。
注目のニュース(ここを強調したい)
これは最初の記事です。
普通のニュース A
これは二番目の記事です。
普通のニュース B
これは三番目の記事です。
/
実務レベルのモダンCSS。
親要素「.news-list」の中で、最初の「article」要素だけを美しく装飾する。
/
.news-list > article:first-of-type {
border: 2px solid var(–primary-color, #0076ff);
background-color: #f0f7ff;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 118, 255, 0.1);
}
/ 2番目以降の要素との間にだけ、適切なマージン(ロブスター・パターン)を設ける /
.news-list > article:not(:first-of-type) {
margin-top: 1rem;
}
パターンB:【万能薬】`:not` 疑似クラスと結合し、隣接兄弟セレクタ(`+`)で制御する
もしどうしても「タグ型がバラバラな中で、特定のクラスを持つ最初の要素」を制御したい場合、`:first-of-type` を使うのは諦めよう。
代わりに、「そのクラスを持つ要素の、直後の同クラス要素」というアプローチをとる。
タイトルヘッダー
警告システム起動中
カードコンテンツ 1
カードコンテンツ 2
カードコンテンツ 3
/
「すべての .card-item」に、まず「最初の要素用」のスタイルを当てる。
(これがデフォルト状態、つまり『最初』のスタイルになる)
/
.mixed-container .card-item {
margin-top: 2rem; / 最初のカードは上部に広い余白を持たせる /
border-left: 4px solid #0076ff;
padding-left: 1rem;
}
/
「.card-item」の直後に続く「.card-item」に対して、スタイルを上書きしてリセットする。
(結果として、2番目以降のカードにだけこのスタイルが適用される)
/
.mixed-container .card-item + .card-item {
margin-top: 1rem; / 2番目以降は詰める /
border-left: 4px solid #ccc; / 通常の色に戻す /
}
この「1番目のスタイルをデフォルトにし、2番目以降を `+`(隣接兄弟セレクタ)で上書きする」手法は、CSSアーキテクチャにおいて極めて堅牢で、IE時代からモダンブラウザに至るまで100%安全に動作する。
—
4. シニアが教える「疑似クラスに頼るべき時、諦めてクラスを付与すべき時」の設計基準
最後に、設計の思想的な話をしよう。
`:first-of-type` や `:nth-of-type` は非常に強力だが、これらに依存しすぎると、「HTMLの構造変更に対して極めて脆いCSS」が出来上がってしまう。
例えば、クライアントから「悪いけど、一番上にちょっとしたお知らせバナーを1行追加して」と言われたとしよう。HTMLの最上部に `
` を1つ追加しただけで、それまで綺麗に動いていた `:first-of-type` のロジックが全崩壊する——そんな悲劇を私は何度も見てきた。
私たちが目指すべきは、変化に強いCSSアーキテクチャだ。以下の基準をチームの共通認識として持っておくといい。
1. 静的で、構造が変わらないことが保証されているセクション(ブログの記事本文、ドキュメント等)
- → `:first-of-type` や `:first-child` を積極的に使って、マークアップを汚さずに美しくスタイリングする。
2. コンポーネントの配置や順序が、運用・CMS・JS制御によって動的に変わる可能性がある場所(カードリスト、ダッシュボードのパネル等)
- → CSSの疑似クラスによる「順番の検知」に依存してはならない。
- → おとなしく、HTML生成側(React / Vue / テンプレートエンジン)で、最初の要素に `.is-first` や `.card-item–featured` のような明示的なクラスを付与する設計にしなさい。
// Reactでの実装例:疑似クラスに頼らず、明示的な状態クラスを付与する
const CardList = ({ items }) => {
return (
{items.map((item, index) => (
))}
);
};
まとめ
- `:first-of-type` は「クラス名」ではなく、「HTMLのタグ名(要素型)」に対して最初の要素を評価する。
- CSSは右から左に解析されるため、`.class:first-of-type` は「そのクラスを持つ最初の要素」にはならない。
- 構造が動的に変わる場合は、CSSの疑似クラスに頼るのではなく、JS側でクラス(修飾子)を動的に付与する方が遥かに堅牢である。
CSSは、ただ画面を飾るための言語ではない。ブラウザのレンダリングプロセスを理解し、破綻しないルールを設計するための「エンジニアリング」だ。
もし明日、後輩が `.my-element:first-of-type` で悩んでいたら、この記事をそっと見せて、ブラウザが右から左へセレクタを解析する仕組みをホワイトボードに書いて説明してあげてほしい。君のチームのコードが、より美しく堅牢になることを願っている。
よりよいエクスペリエンスを提供するため、当ウェブサイトでは Cookie を使用しています。引き続き閲覧する場合、Cookie の使用を承諾したものとみなされます。
タイトルとURLをコピーしました
コメント