フロントエンドの現場で日々コードを書いていると、「親要素の中にいる子要素のどれかにフォーカスが入ったとき、親のスタイルをごっそり変えたい」という要件に必ずぶち当たりますよね。
例えば、検索フォーム。`` と検索ボタンの `
昔の僕たちなら、JavaScriptで `focusin` イベントを監視して、親要素に `.is-focused` みたいなクラスをわざわざ付与して……なんて泥臭い実装をしていました。
でも、もうそんなハックは必要ありません。現代のCSSには、救世主である `:focus-within` 疑似クラス があります。今回は、この強力な疑似クラスの正体と、現場で即座に使える実践的な知見を余すところなく伝授しましょう。
—
1. `:focus-within` とは何か?(仕様とブラウザの裏側の話)
`:focus-within` の基本定義はシンプルです。「その要素自身、またはその子孫要素のいずれかがフォーカスを持っている状態」にマッチします。
「within(〜の内部に)」という名前の通り、DOMツリーの深部にフォーカスが潜り込んだとき、その上流(祖先)にいる要素が「おっ、俺の子分が今注目を浴びているな」と検知してスタイルを変えられるのがこのセレクタの真骨頂です。
ブラウザは裏側でどう処理しているか?
ここで少し、ブラウザのエンジンが裏側でどう動いているかという話をしましょう。これを知っておくと、CSSのパフォーマンスに対する解像度がグッと上がります。
ブラウザ(Blink, Gecko, WebKitなど)は、DOMツリーのフォーカス状態を常にトラッキングしています。ユーザーが `Tab` キーを押したりクリックしたりしてフォーカスが移動すると、ブラウザはフォーカスを持った要素(Active Element)を特定します。
通常であれば、その要素単体が `:focus` にマッチするかどうかを判定して終わりですが、`:focus-within` が使われている場合、ブラウザはそのフォーカス要素からDOMツリーを根(Root)に向かって逆向きに辿る(Bubble upするようなイメージです)処理を行います。
そして、経路上の祖先要素に対して「お前の子孫にフォーカス当たってるよ」というフラグを立て、`:focus-within` のスタイルを適用・再描画(リフロー・リペイント)させます。
「DOMを逆算するなんて重い処理なんじゃないか?」と心配になるかもしれませんが、現代のブラウザの最適化は凄まじく高速です。JavaScriptでイベントリスナーを仕込んでDOMをゴソゴソいじるコストに比べれば、CSSエンジンがネイティブで処理する `:focus-within` は圧倒的に軽量で安全です。安心して使ってください。
—
2. 現場で即戦力になる!実践コード例
百聞は一見にしかず。実務でよく遭遇する3つのパターンをコードベースで見ていき目線を合わせましょう。そのままコピペしてプロジェクトに組み込めます。
パターンA:モダンな検索フォームのコンテナハイライト
これが `:focus-within` の最も美しく、一番よく使われるユースケースです。
/ 検索フォームの親コンテナ:普段はニュートラルなボーダー /
.search-form-group {
display: flex;
align-items: center;
border: 2px solid #ccc;
border-radius: 8px;
padding: 4px 8px;
background-color: #fff;
/ フォーカス時のトランジションを滑らかに仕込んでおくのがプロの技 /
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/
【ここがポイント】
input自体、またはbutton自体にフォーカスが入った瞬間、
親である .search-form-group のスタイルを変化させる
/
.search-form-group:focus-within {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
/ インプット自体のデフォルトの輪郭(アラインメント)は邪魔なので消す /
.search-form-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 1rem;
padding: 8px;
}
/ ボタンも同様にデフォルトのアウトラインをリセットして親に委譲する /
.search-form-button {
background: none;
border: none;
cursor: pointer;
padding: 8px;
}
この実装の美しいところは、インプットにフォーカスが当たっていようが、tabキーで隣の検索ボタンにフォーカスが移動していようが、「ユーザーがこの検索フォーム群のどこかを操作している間は、一貫して親枠が光り続ける」という点です。UXの観点から見ても非常に親切ですよね。
—
パターンB:アクセシビリティを担保したドロップダウンメニュー
アクセシビリティ(a11y)を意識したキーボード操作可能なドロップダウンメニューでも、`:focus-within` は大活躍します。マウスホバー(`:hover`)だけでなく、キーボードユーザーが `Tab` でメニュー内に入ってきたときにもリストを展開させることができます。
.dropdown-menu {
position: relative;
display: inline-block;
}
/ デフォルトではリストを非表示にしておく /
.dropdown-list {
position: absolute;
top: 100%;
left: 0;
display: none; / 本来はvisibilityやopacityでアニメーションさせるのがベター /
list-style: none;
padding: 8px 0;
margin: 4px 0 0;
background: #fff;
border: 1px solid #ddd;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
/
ホバーしている時、または
トリガーボタンやメニュー内のリンクにフォーカスが入っている時にリストを表示する!
/
.dropdown-menu:hover .dropdown-list,
.dropdown-menu:focus-within .dropdown-list {
display: block;
}
.dropdown-list a {
display: block;
padding: 8px 16px;
color: #333;
text-decoration: none;
}
.dropdown-list a:hover {
background-color: #f0f0f0;
}
JavaScriptの `mouseenter` と `focusin` を両方キャッチしてクラスを付け外しするコードを書いていた時代を思い出してください……。あのボイラープレートなコードが、CSSのこの数行で完結してしまうのだから、現代のフロントエンド開発は本当に素晴らしいです。
—
3. 実務でハマりがちな「落とし穴」と回避のコツ
さて、ここからがシニアからのリアルな現場アドバイスです。`:focus-within` は非常に強力ですが、実務で使う上でいくつか注意すべき罠があります。
罠1:IE11の呪縛はもう忘れていいが、古い環境のポリシーを確認せよ
今更言うまでもありませんが、Internet Explorer 11は完全にサポートが終了しています。`:focus-within` は主要モダンブラウザで完全サポートされているため、新規案件やリプレイス案件であれば基本は気にせず使って大丈夫です。
ただし、レガシーな社内システムなどを保守している場合は、未対応ブラウザが存在するリスクがあるので、Can I use等でターゲット層の環境を必ず確認してください。
罠2:「フォーカスが外れた瞬間」のアニメーションの罠
`:focus-within` が外れたとき(フォーカスがロストしたとき)、`transition` を設定していると、パッとスタイルが元に戻る挙動になります。これ自体は自然なのですが、「フォーカスが子孫要素の間で移動するとき(例:インプットからボタンへ)」に注意が必要です。
例えば、親コンテナの中に `input` と `button` があり、両方ともに `:focus-within` でスタイルを変えている場合、
1. `input` にフォーカス (`:focus-within` 適用)
2. `Tab` で `button` へ移動 (`:focus-within` 継続)
この間、親要素は一貫してフォーカス内にあるためスタイルは維持されますが、もし子要素個別の `:focus` スタイルと親の `:focus-within` が複雑に絡み合っていると、意図しないチラつきが発生することがあります。
「親のスタイル制御は `:focus-within` に一任し、子要素個別の派手なスタイル上書きは控えめにする」という役割分担を意識すると、コードが綺麗に破綻なくまとまります。
—
まとめ
`:focus-within` は、単なる「便利なCSSプロパティ」の一つではありません。
これまでJavaScriptに頼らざるを得なかった「コンテキストの変更検知」を、宣言的なCSSの世界に引き戻してくれたアーキテクチャ上の重要ピースです。
JSのコード量を減らし、ブラウザのネイティブ機能に処理をオフロードすることで、アプリケーションのパフォーマンス向上と保守性の高さを同時に手に入れることができます。
もしあなたのプロジェクトで、まだ「親のスタイルを変えるためにJSでクラスをゴソゴソ操作しているコード」を見つけたら、ぜひこの `:focus-within` を使ってリファクタリングを提案してみてください。「おっ、わかってるな」とチームで一目置かれる存在になれるはずです。
明日からのコーディングで、ぜひガンガン活用していきましょう!

コメント