【window.matchMedia】メディアクエリでhoverが使えるデバイスを判定

【window.matchMedia】メディアクエリでhoverが使えるデバイスを判定

はじめに

ドロップダウンメニューとかを作っていると、

  • スマホではタップで開閉
  • PCではマウスオーバー/マウスアウトで表示/非表示
  • タブレットではPCと同じ見た目でタップで表示/非表示

みたいにデバイスごとの挙動を調整する必要が出てきます。

具体的には、PCはマウス、タブレットやスマホはタップが基本UIなので、画面サイズ + hover対応可否の判定が必要になります。

この記事は、そんな時の判定方法についてのメモです。

CSSでhoverの対応可否を判定

まずはメディアクエリでのhover対応可否の判定について。

メディアクエリはいろんな状態を判定できます。
「hover」が使えるかどうかの判定もできます。

@media (any-hover:hover) {
  transition: background-color .3s ease; 
  cursor: pointer;
  
  &:hover{
    background-color: $blue;
  }
}

上記のCSSコードは、any-hoverというメディア特性を使ってhoverの有無を判定しています。hoverの使える画面ではボタンの色を変化させるアニメーションと、マウスカーソルに変化を付けています。

any-hoverには他にnoneが指定できます。

@media (any-hover:none) {
  
}

この場合は、hoverが全く機能しない画面のみに反映されるCSSを書くことができます。

CSSではこのように簡単な記述で判定できます。
ここではany-hoverを使いましたが、他にも細かい状態を判定できるメディア特性が複数あり、組み合わせて使えます。

下記の記事に詳しく書いてあります。

メディア特性を使ってデバイスに併せてhoverのスタイルを出し分ける方法

https://qiita.com/gilly/items/546adecd18a8db3fd059

いろんな判定方法がありますが、個人的には通常のWebサイト作成ではany-hoverでいいかな。

JavaScriptでhoverの対応可否を判定

実はJSでも上述のCSSの内容で紹介したメディアクエリが使えます。

window.matchMediaという関数で簡単に判定できるようになっています。

window.matchMediaで判定

主にCSSでレスポンシブコーディングする際に使う「メディアクエリ」での画面の判定方法をJSから扱える関数です。

下記のように使います。

const mql = window.matchMedia('(min-width: 768px)');
if(mql.matches) {
  // 画面サイズ768px以上
} else {
  // 画面サイズ768px未満
}

これをCSSと同じくany-hoverに合わせて書き換えます。
丁寧に書くとこうなります。

const mql = window.matchMedia('(any-hover:hover)');
if(mql.matches) {
  // hoverが使える
} else {
  // hoverが使えない
}

1行で書くならこう。

const isHover = window.matchMedia("(any-hover:hover)").matches;

これで簡単にhoverの有無を判定できますね。

メディアクエリを使ってスマホ・PC・タブレット(タッチデバイス)を判定する

ここからが本題。

スマホ・PC・タブレット(タッチデバイス)を判定したい時にどう書けばいいか。

  1. 画面サイズ(ブレイクポイント)でスマホかどうか判定。
  2. 画面サイズがスマホ以外の時にhoverが使えるかどうかで判定。

レスポンシブデザインの場合、デザイン段階からスマホの時はhoverを想定していることはほぼないので、1でまずスマホとスマホ以外に絞り込みます。

const mql = window.matchMedia('(min-width: 768px)');
if(mql.matches) {
  // PC・タブレットのデザイン
} else {
  // スマホのデザイン
}

次に2の判定。
PC・タブレットのデザインでhoverが使える場合はPC、それ以外はタブレット(タッチデバイス)と判定します。

const mql = window.matchMedia('(min-width: 768px)');
if(mql.matches) {
  const isHover  = window.matchMedia("(any-hover:hover)").matches;
  if(isHover){
    // PC
  }
  else{
    // タブレット(タッチデバイス)
  }
} else {
  // スマホ
}

これでスマホ・PC・タブレット(タッチデバイス)を判定できました。

正確にはデバイスを判定したわけではないですけどね。
デザイン上の仕分けとしての判定になります。

使えるシチュエーションとしては、

  • PCのhoverとして考えていたUIを同じデザインのままタップUIにも対応したい場合
  • 同じタッチデバイスでもスマホとタブレットでデザインが違う場合に分けて対応したい場合

などですかね。

最終的にできたコード

最終的に下記のようなコードになりました。

class DetectDevice {
  constructor() {
    this.createModels();
    this.bind();
  }

  createModels() {
    this.resizeFlag  = true;
    this.resizeTimer = null;
    //hover判定(hoverが使えるかどうか)
    this.isHover     = window.matchMedia("(any-hover:hover)").matches;
    this.mql         = window.matchMedia('screen and (min-width: 768px)');
    this.isSp        = null;
    this.isTablet    = null;
    this.isPC        = null;
  }

  bind() {
    //初回実行
    window.addEventListener('load', () => {
      this.detect(this.mql);
    });
    
    // リサイズ時にもデバイス判定を更新
    window.addEventListener('resize', () => {
      if(!this.resizeFlag){
        return;
      }

      this.resizeFlag = false;

      if(this.resizeTimer){
        window.clearTimeout(this.resizeTimer);
      }

      this.resizeTimer = window.setTimeout(() => {
        this.detect(this.mql);
        this.resizeFlag = true;
        window.clearTimeout(this.resizeTimer);
      }, 100);

    });
  }

  detect = (mql) => {
    //タブレットかPC
    if (mql.matches) {
      //hover判定の更新
      this.isHover  = window.matchMedia("(any-hover:hover)").matches;
      //タッチデバイスの場合
      if(this.isHover){
        console.log('PC');
        this.isSp     = false;
        this.isTablet = false;
        this.isPC     = true;
      }
      //タッチデバイスでない場合
      else{
        console.log('タブレット(タッチデバイス)');
        this.isSp     = false;
        this.isTablet = true;
        this.isPC     = false;
      }

    }
    //スマホ
    else {
      console.log('スマホ');
      this.isSp     = true;
      this.isTablet = false;
      this.isPC     = false;
    }
  }

}

new DetectDevice();

上述の判定方法に画面サイズの切り替え時の判定もプラスしてデモを作りました。

hoverの対応可否でデバイス判定するデモ

Chromeのデベロッパーツールなどでデバイスやタッチ有無を切り替え・画面リサイズすると、 切り替わった画面に合わせて真ん中の表示が「スマホ」「タブレット(タッチデバイス)」「PC」に変わります。

window.resizeは使いたくない場合

デモではresizeイベントを使って画面サイズの切り替えに対応しています。
ただ、この方法は画面サイズが変更される度に毎回実行されてしまうため負荷がかかってあまり良くないっていう意見があります。そういった点が気になる方はこんな書き方もあります。

const mql = window.matchMedia('(min-width: 768px)');

mql.addEventListener('change', (e) => {
  if(mql.matches) {
    // 画面サイズ768px以上
  } else {
    // 画面サイズ768px未満
  }
})

この書き方にすると、ブレイクポイントが切り替わったタイミングだけでイベントを発生させることができるので効率化して負荷を下げられます。

ただし、ブレイクポイントが複数ある場合はメディアクエリの書き方を変えたり、JSの書き方を変えたり、resizeと違ったアプローチが必要になってきます。

おわりに

JSでmouseentermouseleaveのようなマウス関連のイベントや、touchstarttouchmoveのようなタッチ関連のイベントを使う場合などは、レスポンシブデザインの場合はスマホ・PCだけでなく、タブレットやタッチスクリーンが備わったデバイスを想定する必要があります。この記事の内容ですべて対応できるわけではありませんが、そういう時にメディアクエリでの判定方法について知っておくと便利です。


年始から途切れることなくがっつり仕事が入ったり、子供がコロナで保育園休園したり、手いっぱいでブログが完全に後回しになってました。
まぁ社内で個人的に書かせてもらっているブログなので、特にノルマとかないし別にいいんですけど。笑
ただ、いろいろ使ったり調べたことを整理したりするのに、ブログを書くっていうのが効率良いなとは思っていて。いま携わっている案件で当たり前のように使ってる内容でも少し違う案件に移ると、もう忘れてて、修正や更新する際にまた一から調べて・・・ってことがまぁまぁあります。その点、 このブログは特に最新情報や優れた知見を述べたいわけではなく、自分がポイントとなる部分を整理してメモってる内容なので、身に付く感覚があるし、もし忘れても読み返すと思い出せて重宝してます。

そんな感じなので、自分のためにもなんとか月1投稿でも続けていきたいと思います。