【実務・中級編】 :hover 疑似クラス – CSS実践ガイド

やあ。今日も今日とてCSSの沼にハマっているかい?
フロントエンドの現場にいると、「ボタンにマウスが乗ったら色を変える」なんて、朝飯前……いや、息をするように書く要件だよな。

でも、ちょっと待ってほしい。君が何気なく書いているその `:hover`、本当に「意図した通りの頑健なスタイル」になっているかい?
「スマホでタップした後に色が残り続ける」「マウスがないデバイスなのにホバー状態が解除されない」「パフォーマンスをゴリゴリに削っている」――現場では、この `:hover` の扱いの粗さが原因で、ユーザー体験を損ねているバグに毎日のように遭遇する。

今回は、中級からもう一段階上のシニアへステップアップしたい君に向けて、`:hover` 疑似クラスの基本からブラウザの裏側の動き、そして現場で即戦力になる実践的テクニックを余すところなく伝授しよう。

—

1. そもそも `:hover` とは何か?(標準仕様とブラウザの裏側)

まずは基本の復習と、ブラウザが裏側で何をやっているかの話だ。

`:hover` は、ユーザーがポインティングデバイス(マウスやスタイラスペンなど)で要素を「指し示している(designating)」状態にマッチする動的な疑似クラスだ。

ここで重要かつ見落とされがちなのが、「マウスカーソルが要素の上にある」イコール「`:hover` が発火する」ではないという点。
正確には、ブラウザのポインティングシステムがその要素を「ターゲット」として認識している瞬間だ。

ブラウザのレンダリングパイプラインと `:hover`

ブラウザのエンジン(BlinkやWebKitなど)は、ユーザーがマウスを動かすたびに「Hit Test(ヒットテスト)」と呼ばれる処理を行っている。画面上のどこにどのレイヤーがあるかを計算し、カーソルの座標にある最も手前の要素を特定するんだ。

ここで `:hover` が適用されると、何が起きるか?
そう、スタイルが変化し、要素の形状やサイズ、色が変わる可能性があるため、ブラウザは再レイアウト(Reflow)や再描画(Repaint)を強制される。
もしアニメーションや複雑なプロパティ(`box-shadow` や `transform` 以外)を派手にいじっていたメインスレッドでこれをやると、あっという間にフレームレートが落ち、あの不快なカクつき――いわゆる「ジャダー(Judder)」を引き起こす原因になる。

つまり、たかが `:hover`、されど `:hover` なのだ。

—

2. 現場の落とし穴:「ファットフィンガー」とタッチデバイス問題

実務で最も頭を悩ませるのが、スマートフォンやタブレットなどのタッチデバイスにおける `:hover` の挙動だ。

指でタップした際、多くのモバイルブラウザは「要素をホバー状態にした上で、クリックイベントを発火させる」というエミュレーションを行う。これが原因で、ボタンをタップした後に色が変化したまま元に戻らなくなる(フォーカスが残り続ける)という現象が起きる。ユーザーからすれば「壊れているのか?」と不安になる瞬間だ。

これをスマートに解決するのが、CSSメディアクエリを用いた「真にホバーが可能な環境の検知」だ。

ベストプラクティス:`@media (hover: hover)` の活用

すべての要素に無条件で `:hover` を書く時代は終わった。現代のフロントエンドでは、デバイスが実際にホバーをサポートしているかを判定して適用するのがプロの作法だ。

/ ベースとなるボタンのスタイル /
.c-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
background-color: #2563eb;
color: #ffffff;
border-radius: 0.375rem;
font-weight: 600;
text-decoration: none;
transition: background-color 0.2s ease, transform 0.2s ease;
}

/
ホバーが可能なデバイス(マウス等)でのみ適用する
※ タッチデバイスでの「タップしっぱなし現象」を防ぐ防衛策
/
@media (hover: hover) and (pointer: fine) {
.c-button:hover {
background-color: #1d4ed8;
transform: translateY(-2px); / 浮き上がるようなリッチな演出 /
}
}

/ タッチデバイス(スマホ)でタップされた瞬間(active時)のフィードバック /
.c-button:active {
transform: translateY(0);
background-color: #1e40af;
}

この `@media (hover: hover) and (pointer: fine)` は実務でめちゃくちゃ使う。
`pointer: fine` は、マウスやスタイラスペンなど、精密なポインティングができるデバイスを指す。これを使うことで、指先(大雑把なポインター)のスマホやタブレットを綺麗に除外しつつ、デスクトップ環境でのみリッチなホバー体験を提供できるんだ。

—

3. 実務で使える!ワンランク上の `:hover` テクニック集

ここからは、コードレビューで「おっ、分かってるね」と唸らせるようなテクニックをいくつか紹介しよう。

① 親子関係の連鎖:子要素にホバーされたら親を変える、あるいはその逆

CSSで「親をホバーしたときに子を変える」のは基本(`.parent:hover .child`)だが、逆に「子にホバーされたら親のスタイルを変えたい」という要件は、昔はJavaScriptが必須だった。
しかし、現代のCSSには `:has()` 疑似クラスがある。これと `:hover` を組み合わせると無敵の強さを発揮する。

フロントエンドの未来について

ここにカードの概要が入ります…

/
カード内のリンク(またはカード自体)にホバーしたとき、
カード全体の影を濃くし、画像を引き伸ばすエフェクト
/
.c-card {
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
overflow: hidden;
transition: box-shadow 0.3s ease;
}

/ :has() を使って「内部のリンクがホバーされている状態」を親側でキャッチする /
@media (hover: hover) and (pointer: fine) {
.c-card:has(.c-card__link:hover) {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}

.c-card:has(.c-card__link:hover) .c-card__img {
transform: scale(1.05);
}
}

.c-card__img {
width: 100%;
height: auto;
transition: transform 0.3s ease;
}

JSでイベントリスナーを貼る必要?そんなもの、モダンなCSSの前には不要さ。宣言的に書けるから保守性も段違いに高い。

② 隣接・間接セレクタと組み合わせた「兄弟巻き込みホバー」

リストメニューなどで、よく「ホバーした項目以外の透過度を下げたい(シアー効果)」というデザインがあるだろ? これもCSSだけで美しく実装できる。

.c-nav-list {
display: flex;
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}

.c-nav-item {
transition: opacity 0.2s ease;
}

/ リスト全体がホバーされている時、かつ「自分自身がホバーされていない」要素の透過度を下げる /
@media (hover: hover) and (pointer: fine) {
.c-nav-list:hover .c-nav-item:not(:hover) {
opacity: 0.4;
}
}

`.c-nav-list:hover` でリスト全体にマウスが入ったことを検知し、その中の `:not(:hover)`(つまりマウスが乗っていない兄弟要素)を一括で薄くする。シンプルだけど非常に洗練されたテクニックだ。

—

4. パフォーマンスとアクセシビリティの極意(シニアからの忠告)

最後に、プロとして絶対に守ってほしいルールを2点伝えておく。

1. アニメーションさせるプロパティを厳選する
前述した通り、ホバー時に `width` や `height`、`padding`、`margin` などをアニメーションさせると、レイアウト計算が毎フレーム走り、低スペックなモバイル端末やノートPCで確実にカクつく。
ホバー時の変化は、`transform` と `opacity`、あるいは色の変化(`color`, `background-color` 等の合成処理が軽いプロパティ)に絞るのが鉄則だ。これらはGPUアクセラレーションの恩恵を受けられるため、滑らかに動作する。

2. キーボード操作(フォーカス)との同居を忘れない
マウスユーザーだけに優しくて、キーボードユーザー(タブキーで移動する人)を無視した実装はアクセシビリティの観点から大チョンボだ。
`:hover` を書いたら、必ず `:focus-visible` もセットで書く、これをチームのコーディング規約に叩き込んでおいてほしい。

@media (hover: hover) and (pointer: fine) {
.c-button:hover {
background-color: #1d4ed8;
}
}

/ キーボードフォーカス時にも、ホバー時と同等の視覚フィードバックを必ず与える /
.c-button:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 2px;
background-color: #1d4ed8;
}

—

まとめ

たかが `:hover` されど `:hover`。
ただ色を変えるだけの疑似クラスに思えるかもしれないが、デバイス特性の考慮(`@media (hover: hover)`)、最新のセレクタ(`:has()`)の活用、そしてパフォーマンスとアクセシビリティへの配慮を掛け合わせることで、プロダクトの品質はプロフェッショナルなレベルへと一気に引き上げられる。

明日からのコードレビューでは、後輩たちの書く `:hover` が「ただ動くだけのもの」になっていないか、ぜひチェックしてあげてほしい。

それじゃあ、今日も良いコードを書こう。質問があればいつでも声をかけてくれ!

コメント

タイトルとURLをコピーしました