タッチデバイスでホバースタイルを回避する方法
タッチデバイスではホバー状態が存在しないため、iOS などではタップ時にホバースタイルが適用されて、タップ後もスタイルが維持されることがあります。このような予期しない挙動は、ユーザー体験に悪影響を与える可能性があります。
この記事ではホバーが有効なデバイスを判定し、スタイルを適用する方法を紹介します。
メディアクエリを使用する
ホバーが有効なデバイスかどうかを判定するために、hover
や any-hover
メディアクエリを使用できます。
-
@media (hover: hover)
: 主な入力デバイスがホバーに対応している場合に適用される。(例: デスクトップやノート PC) -
@media (any-hover: hover)
: 入力デバイスのいずれかがホバーに対応している場合に適用される。(例: マウスが接続されているタッチデバイス)
この例では、hover
や any-hover
が無効なデバイスでは打ち消し線を表示しています。
Chrome を利用している場合は、Devtool のデバイスモードを利用して、タッチデバイスをエミュレートし、ホバーの挙動を確認できます。
ただし、これらのメディアクエリを使用する際には注意が必要です。
例えば、iPad にトラックパッドやマウスを接続した場合、デバイスにはタッチとマウスカーソルの2つの入力デバイスが存在します。通常、主な入力モードはタッチであるため、 hover
メディアクエリは適用されませんが、any-hover
メディアクエリは有効になります。さらにブラウザや OS によって主となる入力デバイスの扱いが異なるため、さらに複雑になることがあります。
結果として、以下のような不整合が生じることがあります。
-
hover
を使用した場合- タッチデバイス: ホバースタイルが適用されない
- マウスカーソル: ホバースタイルが適用されない(望ましくない)
-
any-hover
を使用した場合- タッチデバイス: ホバースタイルが適用される(望ましくない)
- マウスカーソル: ホバースタイルが適用される
現状ではメディアクエリのみでこれらの不整合を避けることはできません[1]。
ユーザーの操作環境に応じて、ホバースタイルをより厳密に制御したい場合は、JavaScript を使用する方法があります。
JavaScript でホバーの状態を判定する
Pointer Events API を利用することで、マウスやタッチデバイスの操作を検知し、適切なホバースタイルを適用できます。
Pointer Events は、マウス、タッチ、ペンなどのポインティングデバイスに対して発生するDOMイベントです。このイベントを基に、ホバーの状態を管理するカスタムフックを作成できます。
import { useState, DOMAttributes } from "react";
const useHover = () => {
const [isHovered, setIsHovered] = useState(false);
let hoverProps: DOMAttributes<Element> = {};
// ポインターが要素内に移動したときの処理
hoverProps.onPointerEnter = (e: React.PointerEvent<Element>) => {
// ポインターの種類がタッチデバイスの場合は、ホバーを適用しない
if (e.pointerType === "touch") {
return;
}
setIsHovered(true);
};
// ポインターが領域外に移動したときの処理
hoverProps.onPointerLeave = (e: React.PointerEvent<Element>) => {
// ポインターの種類がタッチデバイスの場合は、ホバーを適用しない
if (e.pointerType === "touch") {
return;
}
setIsHovered(false);
};
return {
hoverProps,
isHovered,
};
};
このコードでは、ホバー状態を管理しています。タッチデバイスが利用されている場合はホバー状態を変更させません。
これにより、タッチデバイスとマウスデバイスの切り替えが可能な場合でも、適切にホバースタイルを当てることができます。
このフックは次のように使用できます。
export default function Page() {
const { hoverProps, isHovered } = useHover();
return (
<button
// ホバー状態を判定するためのイベントハンドラーを追加する
{...hoverProps}
style={{ backgroundColor: isHovered ? "pink" : "lightgray" }}
>
Hover me!
</button>
);
}
この例では、ホバー状態に応じてインラインスタイルを適用していますが、data
属性を利用してスタイルを分離することも可能です。
useHover
フック
おまけ: React Aria の さらに高度なホバーの制御が必要な場合は、React Aria の useHover
が便利です。
このフックを利用すると、要素に disabled
が付与されている場合にはホバーの判定を無効化したり、iOS 13 以前で発生していたタッチイベント後にマウスイベントが発生する不具合や、Portal を介したイベント伝播の問題など、より複雑なユースケースにも対応できます。
詳しくは Building a Button Part 2: Hover Interactions が参考になるので、興味がある方はご覧ください。
まとめ
多用なデバイスが存在する現在では、メディアクエリだけではホバースタイルを適切に制御できない場合があります。そのようなケースでは、JavaScript や React Aria などのユーティリティを活用するのも一つの手段です。
ただし、多くの場合はメディアクエリで対応できるため、実装コストとのバランスを考慮して適切な方法を選択できると良さそうですね。
参考
- hover - CSS: Cascading Style Sheets | MDN
- any-hover - CSS: Cascading Style Sheets | MDN
- Are Hover Events Extinct? | Design Shack
- Finally, a CSS only solution to :hover on touchscreens | by Mezo Istvan | ITNEXT
- Building a Button Part 2: Hover Interactions – React Spectrum Blog
- Pointing the way forward | Blog | Chrome for Developers
- Pointer events - Web APIs | MDN
Discussion