「すいません、CSSで最後の要素だけにマージンを消したいんですけど、なぜか `:last-child` も `:last-of-type` も効かないんです…!」
フロントエンドの現場で、何度この相談を受けたか分かりません。そしてその度に私は「よし、ちょっとお茶でも飲みながら、CSSの『型(Type)』の概念について整理しようか」と微笑むのです。
こんにちは。チームのフロントエンド領域のアーキテクチャを担当しているシニアエンジニアです。
今日は、一見シンプルに見えて、実は仕様の誤解から現場のバグの温床になりがちな `:last-of-type` 疑似クラス について、ブラウザの裏側の挙動を交えて徹底的に解説します。この記事を読めば、もう二度と「意図しない要素にスタイルが適用されてしまう」「なぜか効かない」といった泥臭いデバッグに時間を溶かすことはなくなります。準備はいいですか?
—
1. 多くのエンジニアが陥る「クラス名指定」の罠
まず、現場で最も頻発する「勘違い」から始めましょう。
以下のようなHTMLがあるとします。
テキスト 1
テキスト 2
テキスト 3(これが実質最後のテキスト)
ここで、「クラス名が `.text` である最後の要素(テキスト 3)だけ、下部のマージンをゼロにしたい」という要件が出たとします。
多くの初・中級エンジニアが、最初に以下のようなCSSを書いて撃沈します。
/ ❌ 意図通りに動かないコード /
.container .text:last-of-type {
margin-bottom: 0;
}
「`.text` というクラスを持つ要素の中で、最後のやつに効くはず!」
そう直感的に思ってしまいますよね。しかし、これは動きません。 正確に言うと、このCSSは「`text 3`」ではなく、「`text 2`」または「どの要素にも効かない(あるいは意図しない挙動)」になります。
なぜでしょうか? ここに `:last-of-type` の本質的な仕様が隠されています。
—
2. 仕様を解剖する:ブラウザは裏側でどう処理しているか?
CSSの仕様において、`:last-of-type` は 「同じ親要素の中で、その『要素型(タグ名)』が最後である要素」 を選択します。
ここが超重要です。「クラス名(.text)」は一切無視され、「タグ名(p や div)」だけで判定されるのです。
先ほどのCSS `.container .text:last-of-type` をブラウザが解釈するプロセスを、脳内でトレースしてみましょう。
1. まず右側(キーセレクタ)を見る: ブラウザは `:last-of-type` を見つけます。
2. 要素型でグループ化する: 親要素(`.container`)の直下にある子要素を、タグ名ごとに分類します。
- `p` タグのグループ: `テキスト 1`, `テキスト 2`, `テキスト 3`
- `div` タグのグループ: `バナー広告`, `補足テキスト`
3. それぞれのグループの「最後」を特定する:
- `p` タググループの最後 = `テキスト 3`
- `div` タググループの最後 = `補足テキスト`
4. 左側のセレクタ条件と照らし合わせる:
- 「ステップ3で抽出された要素のうち、`.text` というクラスを持っているものはどれだ?」
- `テキスト 3`(pタグ) ➡ `.text` クラスを持っている ➡ マッチする!
- `補足テキスト`(divタグ) ➡ `.text` クラスを持っていない ➡ マッチしない
「あれ? 動いてるじゃないか」と思いましたか?
では、HTMLが以下のように変わったらどうでしょう。
テキスト 1
テキスト 2
テキスト 3
この場合、
- `p` グループの最後は `テキスト 3`(`.text` あり ➡ マッチする)
- `div` グループの最後は `バナー広告`(`.text` なし ➡ マッチしない)
ここまではいいですね。では、もし「最後のp要素」には `.text` クラスがついておらず、別のクラス(例えば `.text-disabled`)がついていたら?
テキスト 1
テキスト 2
テキスト 3(非活性)
この状態で `.container .text:last-of-type` を適用すると、
- `p` グループの最後は `テキスト 3` です。
- しかし、`テキスト 3` は `.text` クラスを持っていません。
- 結果として、何も選択されず、マージンはどこも消えません。(`テキスト 2` のマージンを消したかったのに!)
このように、`:last-of-type` は「クラスのフィルタリング」を先に行うのではなく、「タグ名でのグループ化」を先に行い、その後にクラス名でフィルタリングする という挙動をします。これが、現場で「効かない!」と叫ばれる最大の原因です。
—
3. 実務で使える綺麗なコード例
では、実際の現場でこの挙動をスマートに制御するにはどうすればいいのか。
そのままコピー&ペーストして、ブラウザで挙動を確認できる実践的なコードを用意しました。
HTML
新着ドキュメント一覧
記事 A
これは最初の記事です。
スポンサー広告
リストの途中に広告用のdivが挟まる実務的なケースです。
記事 B
これが「sectionタグ」としては最後に出現する要素です。
補足コラム
最後にarticleタグの要素が配置されています。
CSS (style.css)
/ ベースのスタイリング(見栄えを整えるためのもの) /
body {
font-family: ‘Helvetica Neue’, Arial, sans-serif;
background-color: #f5f7fa;
padding: 40px;
display: flex;
justify-content: center;
}
.card-container {
background: #ffffff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
width: 100%;
max-width: 500px;
}
.card-title {
margin-top: 0;
margin-bottom: 20px;
font-size: 1.25rem;
color: #333;
border-bottom: 2px solid #ddd;
padding-bottom: 8px;
}
/
- 実務的なスタイリング
/
.card-item {
padding: 16px;
margin-bottom: 16px;
border-radius: 6px;
background-color: #fcfcfc;
border: 1px solid #eef2f5;
}
/
- 💡 :last-of-type の検証
- .card-item というクラス指定がありますが、ブラウザは「タグ名(section)」ごとに最後を判定します。
- したがって、ここでは「最後の
である 記事B」にスタイルが適用されます。 - 末尾の「補足コラム(article)」や途中の「広告(div)」には影響しません。
/
section.card-item:last-of-type {
border-left: 4px solid #3b82f6; / 青いアクセントライン /
background-color: #eff6ff; / ほんのり青みがかった背景 /
}
/
- 💡 最後の
にスタイルをあてる
/
article.card-item:last-of-type {
border-left: 4px solid #10b981; / 緑のアクセントライン /
background-color: #ecfdf5; / ほんのり緑がかった背景 /
margin-bottom: 0; / コンテナの最後なのでマージンを消去 /
}
このコードを実行すると、`section` の最後である「記事 B」と、`article` の最後である「補足コラム」に、それぞれ意図したスタイルがピンポイントで適用されるのが確認できるはずです。
—
4. チーフアーキテクトが語る現場のベストプラクティス
「仕様は分かった。でも、タグ名に依存するコードは、将来HTMLのタグが `section` から `div` に変更された瞬間に壊れるから怖い…」
その直感、素晴らしいです。まさに堅牢なCSS設計(CSS Architecture)の視点ですね。
実務で `:last-of-type` を安全に扱うため、また、どうしても「特定のクラスの最後」をコントロールしたい場合の設計パターンを2つ伝授します。
解決策A:タグ型を統一し、堅牢なBEMでカプセル化する
HTMLの構造を整理できるのであれば、リスト内の要素のタグ型を統一し、`:last-child` を使うのが最もシンプルで頑丈です。
/ タグが統一されていれば、:last-child で確実に最後の要素を狙い撃ちできる /
.list__item:last-child {
margin-bottom: 0;
}
解決策B:最新の強力な武器 `:has()` 疑似クラスを使う(モダンブラウザ対応)
現在、主要なブラウザすべてで `:has()` 疑似クラス(CSS Parent Selector)が使えるようになりました。これを使えば、「同じクラス名を持つ要素の中で、実質的に最後に出現するもの」 を完全にCSSだけで選択できます。
これこそが、かつて我々が諦め、JavaScriptでクラスを付与していた挙動の「正解」です。
/
- 後続に「同じ .card-item」が存在しない、
- つまり「.card-item の中で一番最後に出現する要素」をセレクトする!
/
.card-item:not(:has(~ .card-item)) {
border: 2px solid #f59e0b; / 最後のカードだけオレンジの枠線に /
}
この `~`(後続シブリングセレクタ)と `:not(:has(…))` の組み合わせは、現代のCSSにおける強力なテクニックです。「後続に自分自身と同じクラスを持つ要素が存在しない = 自分が最後である」というロジックを表現しています。実務でこれを使うと、マークアップの自由度が劇的に向上します。
—
5. まとめ:仕様を味方につけて、泥臭いデバッグから卒業しよう
`:last-of-type` は決して「使えない古い疑似クラス」ではありません。
「要素型(Type)に基づいて最後を選択する」という、明確な役割を持った強力なセレクタです。
- `:last-child` は、親要素の中の「最後の子要素」が、指定した条件に合致するかを見る。
- `:last-of-type` は、親要素の中の「タグ名ごとのグループ」を作り、それぞれのグループの最後を見る。
- クラス名での絞り込みを期待して `:last-of-type` を使うのはNG。
- タグ名が混在するリストで「特定のクラスの最後」を狙うなら、`:has()` 疑似クラス の活用を検討する。
ブラウザがHTMLをどのようにパースし、セレクタを評価しているのか。その裏側のロジックを理解しておけば、どんな複雑なレイアウト要件が来ても涼しい顔でエレガントなCSSを書くことができます。
チームの後輩たちがマークアップで迷っていたら、ぜひこの「タグ名グループ化」の話を教えてあげてください。
一歩ずつ、より堅牢で美しいフロントエンドを築いていきましょう。応援しています!

コメント