やあ、現場の最前線でコードと格闘している同志諸君。今日も元気にブラウザのレンダリングエンジンと知恵比べをしているかな?
今日は、意外と知られていないが、使いこなすと「JavaScriptなしでここまでできるのか!」とチームを驚かせることができる強力な武器について話をしよう。テーマは「ターゲット要素の状態を親へ伝播させる設計」だ。
実は、君たちが探しているであろう `:target-within` という疑似クラスは、現在の公式なCSS仕様(Selectors Level 4時点)には直接的には存在しない。しかし、現代のモダンブラウザが手にした究極の「後方参照」セレクタである `:has()` を使うことで、実質的に `:target-within` と呼ぶべき挙動を完璧に、しかもエレガントに実装できる。
「なんだ、`:has()` を使うのか」と侮るなかれ。この `:has(:target)` の組み合わせが、どれほど実務の泥臭い課題をスマートに解決するか、設計の観点から深掘りしていこう。
—
1. なぜ「ターゲット内」の状態を親が知る必要があるのか?
フロントエンドの設計をしていると、URLのフラグメント(`#section-1` など)によって特定の要素がアクティブになった際、その要素自体だけでなく、それを含むコンテナ全体やナビゲーションの親要素の見た目を変えたいというシーンに必ず遭遇する。
例えば、以下のようなケースだ。
- アコーディオンの特定項目がリンクで開かれた際、その親要素のボーダーを強調したい。
- サイドバーの深い階層にあるリンクがターゲットになったとき、親のツリーを自動的に展開状態(ハイライト)にしたい。
- タブコンテンツ内で特定のアンカーが指定された際、タブの枠組み全体を「アクティブモード」の装飾に切り替えたい。
これまでは、わざわざ数行のJavaScriptを書いて `hashchange` イベントを監視し、親要素に `is-active` なんてクラスを付け外ししていたはずだ。だが、そんな「状態管理の断片」をJSに散らすのは、もう終わりにしよう。
2. 理論:`:has(:target)` が裏側でやっていること
ブラウザが `:has(:target)` をどう解釈しているか、少しマニアックな視点で覗いてみよう。
通常、CSSのセレクタは「親から子」へと流れる。ブラウザのレンダリングエンジンにとって、親のスタイルを決定する際に「その子孫が今どんな状態(`:target`)か」を確認しにいくのは、計算コストが高い処理だった。だからこそ、長年この手のセレクタは禁忌とされてきたんだ。
しかし、近年のブラウザ最適化技術により、この「条件付き後方参照」が可能になった。
- `URL` の末尾に `#my-id` が付く。
- ブラウザは ID `my-id` を持つ要素を `:target` 状態にする。
- `:has(:target)` を持つ親要素は、DOMツリーを下向きにスキャンし、ターゲット状態の要素が含まれているかを判定する。
これが実質的な `:target-within` の正体だ。
—
3. 実戦コード:JS不要の「自動ハイライト・セクション」
さあ、御託はいい。実際にそのままプロジェクトへ放り込めるコードを見てみよう。
ここでは、特定のFAQやコンテンツがURL経由で指定された際、その親カード全体をリッチに装飾する例を紹介する。
トピック A: CSSの設計思想
ここには深い知見が書かれている。URLで直接飛ばされてくる価値がある場所だ。
トピック B: セレクタの計算量
パフォーマンスを意識するなら、セレクタの書き方にも拘るべきだ。
/ ————————————————–
- 実装の肝:疑似的な :target-within
- ————————————————– /
/
- .card の子孫要素のいずれかが :target 状態(URLの#と一致)なら、
- 親である .card 自体にスタイルを適用する。
/
.card:has(:target) {
border-color: #007bff;
background-color: #f0f7ff;
box-shadow: 0 0 20px rgba(0, 123, 255, 0.2);
transform: translateY(-2px);
outline: 2px solid #007bff;
outline-offset: 4px;
}
/ ターゲットそのものへの装飾(従来通り) /
:target {
color: #0056b3;
transition: all 0.3s ease;
}
/ 現場で役立つアクセシビリティへの配慮:
ターゲットへジャンプした際、ヘッダーに隠れないよう余白を作る /
[id] {
scroll-margin-top: 100px;
}
/ 装飾用の基本スタイル /
.card {
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
—
4. プロの視点:運用上の注意点とベストプラクティス
この `:has(:target)` (疑似 `:target-within`)を実務で導入する際、シニアエンジニアとして君たちに伝えておきたい「泥臭い」アドバイスがいくつかある。
① ブラウザサポートの現実
`:has()` は現在、主要なモダンブラウザ(Chrome, Edge, Safari, Firefox)の最新版でフルサポートされている。だが、古いIEの亡霊や、極端に古いAndroid端末をサポートしなければならない案件では、まだ「銀の弾丸」にはならない。その場合は、Polyfillを入れるよりも、おとなしくJSで `classList.toggle` を書くのが大人の判断だ。
② セレクタのスコープを絞る
`:has()` は強力すぎるがゆえに、広範囲(例えば `body:has(:target)`)に適用すると、DOMの規模によっては再計算コストが無視できなくなる可能性がある。今回のように `.card:has(:target)` と、適用範囲を明確なコンポーネント単位に絞るのが、パフォーマンスを維持するコツだ。
③ UXの隠し味
`:target` は、ページ内リンクをクリックした時だけでなく、ブラウザの「戻る・進む」ボタンでも発火する。`:has(:target)` で親要素を強調しておくことで、ユーザーは「今、ページのどこに焦点が当たっているのか」を視覚的に瞬時に理解できる。これはアクセシビリティの観点からも非常に優れたアプローチなんだ。
—
まとめ:宣言的UIの極致へ
`:target-within` という名前の仕様を待つ必要はない。我々にはすでに `:has(:target)` という、より汎用的で強力な武器がある。
「状態(State)」をJavaScriptで管理するのは、ロジックが必要な時だけでいい。
URLとDOM構造だけで完結する「見た目の状態変化」は、CSSに任せるのが一番クリーンで、メンテナンス性が高い。
今日から君のプロジェクトでも、この「親が子のターゲット状態を検知する」テクニックを使って、コードをより宣言的(Declarative)に書き換えてみてくれ。チームのメンバーが「え、これJSどこでやってるの?」と驚いたら、ニヤリと笑ってこの記事をシェアしてあげてほしい。
では、また次のコードレビューで会おう。良いエンジニアライフを!

コメント