🕊️

タッチデバイスでホバースタイルを回避する方法

2024/08/16に公開

タッチデバイスではホバー状態が存在しないため、iOS などではタップ時にホバースタイルが適用されて、タップ後もスタイルが維持されることがあります。このような予期しない挙動は、ユーザー体験に悪影響を与える可能性があります。

この記事ではホバーが有効なデバイスを判定し、スタイルを適用する方法を紹介します。

メディアクエリを使用する

ホバーが有効なデバイスかどうかを判定するために、hoverany-hover メディアクエリを使用できます。

  • @media (hover: hover): 主な入力デバイスがホバーに対応している場合に適用される。(例: デスクトップやノート PC)
  • @media (any-hover: hover): 入力デバイスのいずれかがホバーに対応している場合に適用される。(例: マウスが接続されているタッチデバイス)

この例では、hoverany-hover が無効なデバイスでは打ち消し線を表示しています。
Chrome を利用している場合は、Devtool のデバイスモードを利用して、タッチデバイスをエミュレートし、ホバーの挙動を確認できます。

ただし、これらのメディアクエリを使用する際には注意が必要です。

例えば、iPad にトラックパッドやマウスを接続した場合、デバイスにはタッチとマウスカーソルの2つの入力デバイスが存在します。通常、主な入力モードはタッチであるため、 hover メディアクエリは適用されませんが、any-hover メディアクエリは有効になります。さらにブラウザや OS によって主となる入力デバイスの扱いが異なるため、さらに複雑になることがあります。

結果として、以下のような不整合が生じることがあります。

  • hover を使用した場合
    • タッチデバイス: ホバースタイルが適用されない
    • マウスカーソル: ホバースタイルが適用されない(望ましくない)
  • any-hover を使用した場合
    • タッチデバイス: ホバースタイルが適用される(望ましくない)
    • マウスカーソル: ホバースタイルが適用される

現状ではメディアクエリのみでこれらの不整合を避けることはできません[1]
ユーザーの操作環境に応じて、ホバースタイルをより厳密に制御したい場合は、JavaScript を使用する方法があります。

JavaScript でホバーの状態を判定する

Pointer Events API を利用することで、マウスやタッチデバイスの操作を検知し、適切なホバースタイルを適用できます。

Pointer Events は、マウス、タッチ、ペンなどのポインティングデバイスに対して発生するDOMイベントです。このイベントを基に、ホバーの状態を管理するカスタムフックを作成できます。

useHover.ts
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 属性を利用してスタイルを分離することも可能です。

おまけ: React Aria の useHover フック

さらに高度なホバーの制御が必要な場合は、React Aria の useHover が便利です。
このフックを利用すると、要素に disabled が付与されている場合にはホバーの判定を無効化したり、iOS 13 以前で発生していたタッチイベント後にマウスイベントが発生する不具合や、Portal を介したイベント伝播の問題など、より複雑なユースケースにも対応できます。

詳しくは Building a Button Part 2: Hover Interactions が参考になるので、興味がある方はご覧ください。

まとめ

多用なデバイスが存在する現在では、メディアクエリだけではホバースタイルを適切に制御できない場合があります。そのようなケースでは、JavaScript や React Aria などのユーティリティを活用するのも一つの手段です。
ただし、多くの場合はメディアクエリで対応できるため、実装コストとのバランスを考慮して適切な方法を選択できると良さそうですね。

参考

脚注
  1. CSSWG ではホバー可能なデバイスでのみ適用されるメディアクエリの提案がありますが、今のところ進展はないようです。 ↩︎

GitHubで編集を提案
サイボウズ フロントエンド

Discussion