スマートフォン向けにボタンを大きくしたい、タッチ操作のときはホバーの表示をやめたい。そんなときに、「タッチ端末かどうか」を JavaScript で判定する書き方がよく使われてきました。しかし今は、タッチ画面とマウスの両方を持つ端末が珍しくないため、この判定は当てになりません。ここでは、判定の問題点と、入力の種類ごとに分ける今の方法を説明します。

よく使われてきた判定と、その問題

代表的なのは、タッチのイベントがあるかどうかを調べる書き方です。

// よく見かける判定 (今は勧められない)
const isTouch = "ontouchstart" in window;

スマートフォンが iPhone と Android の一部の機種に限られていたころは、これで「スマートフォンかどうか」をほぼ見分けられました。今は、次のような端末で判定が外れます。

  • タッチ画面付きのノートパソコン。タッチのイベントはあるが、ふだんはマウスやタッチパッドで操作している
  • マウスやキーボードをつないだタブレット。タッチでもマウスでも操作する
  • iPad の Safari。パソコン向けの表示を求める設定が標準のため、ユーザーエージェント (ブラウザーが名乗る文字列) が Mac と同じになっていて、文字列からは iPad と分からない

navigator.maxTouchPoints (同時に触れられる指の数) で調べる方法もありますが、分かるのは「タッチ画面がある」ことだけです。利用者が今どの入力を使っているかまでは分かりません。

タッチ端末だと判定してホバーの表示を消すと、マウスで操作している人にはホバーの手がかりが無くなります。逆に、パソコンだと判定してボタンを小さくすると、指で操作する人が押しにくくなります。端末の種類を当てるのではなく、「どんな入力で操作されているか」に合わせるのが今の考え方です。

CSS のメディア特性で分ける

CSS のメディアクエリには、入力の性質を調べるメディア特性があります。

  • hover: 主な入力 (いちばんよく使われる入力) で、要素の上にポインターを乗せられるか。マウスなら hover、指なら none
  • pointer: 主な入力で、どれくらい細かい位置を指せるか。マウスなら fine、指なら coarse (おおまか)、ポインターが無ければ none
  • any-hover、any-pointer: 主な入力に限らず、使える入力のどれか 1 つでも当てはまるか
/* ホバーできる入力が主のときだけ、ホバーの見た目を付ける */
@media (hover: hover) {
  .card:hover {
    box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
  }
}

/* 指で操作するのが主のときは、押せる範囲を広げる */
@media (pointer: coarse) {
  .toolbar button {
    min-width: 44px;
    min-height: 44px;
  }
}

/* 精度の低い入力が 1 つでもあれば、項目のあいだを空ける */
@media (any-pointer: coarse) {
  .menu li + li {
    margin-top: 8px;
  }
}

タッチ操作では、指で触れた要素にホバーの見た目が残ってしまうことがあります。ホバーの指定を (hover: hover) の中に入れておくと、この「消えないホバー」を避けられます。

ただし、どの値になるかは端末やブラウザーの判断によります。ホバーでしか見られない情報や、細かい操作でしか押せない部品は、どの入力でも困らないように作っておくのが基本です。

JavaScript では matchMedia を使う

同じメディア特性は、JavaScript の matchMedia() でも調べられます。マウスをつないだり外したりすると結果が変わることがあるので、change イベントで追いかけます。

const coarse = matchMedia("(pointer: coarse)");

function applyPointerClass() {
  // 指での操作が主なら、html 要素に class を付ける
  document.documentElement.classList.toggle("is-coarse", coarse.matches);
}

applyPointerClass();
coarse.addEventListener("change", applyPointerClass);

見た目を変えるだけなら CSS のメディアクエリで足ります。matchMedia() は、ドラッグで並べ替える部品の操作方法を変えるなど、処理を分けたいときに使います。

操作ごとに入力を知るなら Pointer Events

その操作がマウス・指・ペンのどれによるものかを知りたいときは、Pointer Events を使います。マウスとタッチとペンの操作を、pointerdown、pointermove、pointerup などの共通のイベントで受け取れる仕組みで、pointerType プロパティに入力の種類が入ります。

const canvas = document.querySelector("#board");

// startStroke() は、線を描き始める処理 (別に用意しておく)
canvas.addEventListener("pointerdown", (event) => {
  if (event.pointerType === "touch") {
    // 指: 線を太めにする
    startStroke(event, { width: 8 });
  } else if (event.pointerType === "pen") {
    // ペン: 筆圧 (0〜1) を線の太さに使う
    startStroke(event, { width: 2 + event.pressure * 6 });
  } else {
    // マウス
    startStroke(event, { width: 4 });
  }
});

1 つの端末で指とペンを持ち替えても、操作のたびに正しく分けられます。「この端末はタッチ端末だ」と最初に決めつける方法では、こうした使い方に対応できません。

まとめ

  • 'ontouchstart' in window は「タッチ画面があるか」しか分からず、タッチ画面付きのパソコンやマウスをつないだタブレットで判定が外れる
  • 見た目は CSS の hover、pointer、any-hover、any-pointer メディア特性で分ける
  • ホバーの見た目は @media (hover: hover) の中に書くと、タッチ操作でホバーが残らない
  • JavaScript では matchMedia() と change イベントで調べる
  • 操作ごとの入力の種類は、Pointer Events の pointerType で分かる

参考