【CSS】まだホバー時のスタイルを :hover だけで指定してるの?
はじめに結論から
ホバースタイルは、 :hover
だけで指定するのではなく、次のように指定しましょう!
@media (hover: hover) {
/* リンクの場合 */
a:any-link:hover {
}
/* ボタンの場合 */
button:enabled:hover {
}
/* 特定できない場合 */
.button:where(:any-link, :enabled, summary):hover {
}
}
ポイント 1 マウスのときだけホバースタイルを当てる
:hover
擬似クラスで指定したスタイルは、タッチデバイスの場合フォーカス状態で適用されてしまいます。
つまり、タッチしたあとのスタイルがずっとホバースタイルのままになってしまいます。
これは意図と合わないため、マウスで操作しているかどうかを区別してスタイルを当てる必要があります。
マウス(正確には、ホバーができる入力手段かどうか)を区別するには、 hover
メディア特性を使用します。
次のように指定することで、マウス操作のときだけホバースタイルを当てることができます。
@media (hover: hover) {
a:hover {
background-color: red;
}
}
マウス操作もタッチ操作もできるデバイスの場合
Surface のような「マウス操作もタッチ操作もできるデバイス」は @media (hover: hover)
の対象となるため、タッチ操作した場合は、ホバー時のスタイルが残り続けてしまいます。
これも区別しようと思うと what-input で動的に区別する必要がありますが、そこまでする(JS を追加する)ほど優先度の高い問題ではないと考えて、受け入れることにしています。
NG 例
マウス操作かどうかを区別する方法としてありがちな NG 例は、ウィンドウ幅のメディアクエリを使用することです。
この方法では、パソコンで狭い幅で表示している場合にホバースタイルが当たらなくなってしまいますし、逆に幅の広いタブレットではホバースタイルが適用されてしまいます。
@media (min-width: 768px) {
a:hover {
background-color: red;
}
}
ポイント 2 クリック可能な要素にだけホバースタイルを当てる
まず意識しておきたいのは、 ホバー時のスタイル変化は、「これをクリックしたら何かが起こりますよ~」ということを伝えるためのものです。
(そうではない装飾目的のものももちろんありますが、それはここでは考えません)
- リンクをクリックしたら別のページに遷移する
- カルーセルの矢印ボタンをクリックしたらスライドが切り替わる
- アコーディオンがクリックしたら展開する
など…
逆に言えば、 クリックして動作が起こらないときにはホバースタイルを当てるべきではありません。
ホバーで反応があったら、クリックして何かが起こることを予想しますが、何も起こらなかったら混乱します。
クリックしてアクションが起こる要素にだけホバースタイルが当たるように、セレクタを工夫する必要があります。
リンクかどうかを区別する
a
要素は、 href
属性が存在する場合はリンクです。
しかし、href
属性がなければリンクではなくなり、クリックしても何も起こりません。
a 要素に href つけないことなんてあるの?と思う方もいるかもしれませんが、
ナビゲーションやパンくずリストなどで現在のページはリンクにしないようにするために、a 要素に href をつけずに使うことがあります。
(HTML の仕様としても間違いではありません)
<a href="/">他のページ</a>
<a aria-current="page">現在のページ(リンクではない)</a>
リンクかどうかを区別するには :any-link
疑似クラスが使用できます。
a:any-link {
}
:any-link
擬似クラス
:any-link
擬似クラスは、href
属性がある a
要素(と、area
要素)にのみマッチするセレクタです。
リンクを対象とする擬似クラスには :link
や :visited
もありますが、:any-link
は閲覧状況によらずマッチします。
[href]
との違い
[href]
で選択するのと変わらないかと思ったのですが、MDN の「ブラウザーの互換性」のセクションに書いてある
:any-link privacy: selector does not match <link> elements
という文を読むに、 :any-link
は <link href="">
にマッチしないようになっているらしいです(Safari 以外は)。
(まあ CSS を書く分には気にすることはないと思いますが)
ボタンが無効になっていないかどうかを区別する
button
や input
は disabled
属性がついている場合、クリックできません。
「カルーセルの最後のスライドに到達していて、「次へ」のボタンを無効にしている」など、クリックできないボタンがある場合は、ホバースタイルを当てないようにする必要があります。
クリックできるかどうかを区別するには :enabled
疑似クラスを使用します。
button:enabled {
}
:enabled
擬似クラス
- disabled でない
button
要素 - disabled でない
input
要素 - disabled でない
select
要素 - disabled でない
textarea
要素
が対象になります。
:not(:disabled)
との違い
:not(:disabled)
でもいいように見えますが、この書き方ではそもそも disabled の概念が存在しない要素(div や span など)が対象になってしまいます。
:enabled
は disabled/enabled の概念がある要素のみが対象になります。
summary 要素かどうかを区別する
summary
要素は疑似クラスでは識別できないので、要素セレクタを使用します。
クラスセレクタの後ろに付ける場合、:is()
や :where()
が使えます。
.toggle:where(summary) {
}
まとめて書くと
あるクラスが、a 要素に設定されるかもしれないし、button 要素に設定されるかもしれないし、summary 要素に設定されるかもしれないという場合は、次のように書くことができます。
.button:where(:any-link, :enabled, summary):hover {
background-color: red;
}
総合すると
こうなります。
@media (hover: hover) {
.button:where(:any-link, :enabled, summary):hover {
background-color: red;
}
}
Sass mixin にしておくと便利
Sass では次のように mixin を用意して使用すると便利です。
@mixin hover {
@media (hover: hover){
&:where(:any-link, :enabled, summary):hover {
@content;
}
}
}
.button {
@include hover {
background-color: red;
}
}
おまけ・クリック可能な親要素がホバーされたときのスタイル
「リンク(a
)の中の画像(img
)を拡大する」といったホバー演出などで使えます。
親要素は「クリック可能な要素かどうか」で選択しているので、わざわざ親要素側のクラスを特定する必要はありません。
(クリック可能な要素がネストされることはないので)
:where(:any-link, :enabled, summary):hover .image {
transform: scale(1.1);
}
別記事で紹介した後置修飾スタイルで書けばこう書けます。
.image:is(:where(:any-link, :enabled, summary):hover *) {
transform: scale(1.1);
}
これも mixin にしておくと便利です。
@mixin group-hover {
@media (hover: hover){
&:is(:where(:any-link, :enabled, summary):hover *) {
@content;
}
}
}
.image {
@include group-hover {
transform: scale(1.1);
}
}
Discussion