サイトにタッチを追加する

スマートフォンからデスクトップ画面まで、より多くのデバイスでタッチスクリーンが利用できるようになりました。アプリは、タップに直感的かつ美しい方法で反応する必要があります。

スマートフォンからデスクトップ画面まで、ますます多くのデバイスでタッチスクリーンが利用されています。ユーザーが UI を操作することを選択した場合、アプリはそのタップに直感的に反応する必要があります。

要素の状態に応答する

ウェブページの要素をタップまたはクリックしたときに、サイトが実際にその要素を検出したのか疑問に思ったことはありませんか?

ユーザーが UI の一部をタップまたは操作したときに要素の色を変更するだけで、サイトが機能しているという基本的な安心感を得ることができます。これにより、フラストレーションが軽減されるだけでなく、テンポ良く応答性に優れた印象を与えることもできます。

DOM 要素は、デフォルト、フォーカス、ホバー、アクティブのいずれかの状態を継承できます。これらの各状態に対して UI を変更するには、擬似クラス :hover:focus:active にスタイルを適用する必要があります。コードは次のようになります。

.btn {
  background-color: #4285f4;
}

.btn:hover {
  background-color: #296cdb;
}

.btn:focus {
  background-color: #0f52c1;

  /* The outline parameter suppresses the border
  color / outline when focused */
  outline: 0;
}

.btn:active {
  background-color: #0039a8;
}

試してみる

ボタンの状態のさまざまな色を示す画像

ほとんどのモバイル ブラウザでは、要素がタップされると、カーソルを合わせた状態やフォーカスされた状態が要素に適用されます。hoverhover

設定するスタイルと、タップ後のスタイルについて慎重に検討してください。

デフォルトのブラウザ スタイルを抑制する

さまざまな状態に対応したスタイルを追加すると、ほとんどのブラウザはユーザーのタップに応じて独自のスタイルを実装していることがわかります。これは主に、モバイル デバイスがリリースされた当初、多くのサイトで :active 状態のスタイルが設定されていなかったことが原因です。その結果、多くのブラウザで、ユーザーのフィードバックに対応するためにハイライトの色やスタイルが追加されました。

ほとんどのブラウザでは、outline CSS プロパティを使用して、要素がフォーカスされたときに要素の周囲にリングを表示します。次のコマンドで抑制できます。

.btn:focus {
    outline: 0;

    /* Add replacement focus styling here (i.e. border) */
}

Safari と Chrome ではタップのハイライト表示の色が追加されます。これは、-webkit-tap-highlight-color CSS プロパティを使用することで回避できます。

/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
  -webkit-tap-highlight-color: transparent;
}

試してみる

Windows Phone の Internet Explorer でも同様の動作を行いますが、次のメタタグを使用して抑制されます。

<meta name="msapplication-tap-highlight" content="no">

Firefox には、対処すべき副作用が 2 つあります。

タップ可能な要素に枠線を追加する -moz-focus-inner 疑似クラスは、border: 0 を設定して削除できます。

Firefox で <button> 要素を使用している場合は、グラデーションが適用されます。これは、background-image: none を設定することで解除できます。

/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
  background-image: none;
}

.btn::-moz-focus-inner {
  border: 0;
}

試してみる

ユーザー選択を無効にする

UI を作成する際、ユーザーに要素を操作させたいものの、長押しでテキストを選択したり UI 上でマウスをドラッグしたりするデフォルトの動作を抑制したい場合があります。

これは user-select CSS プロパティで行うことができますが、ユーザーが要素内のテキストを選択したい場合、コンテンツに対してこれを行うとextremely不快な思いをさせる可能性があるので注意してください。使用する際は慎重に行ってください。

/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
  user-select: none;
}

カスタム ジェスチャーを実装する

サイトのカスタム操作とジェスチャーについてアイデアがある場合は、次の 2 つのトピックを念頭に置いてください。

  1. すべてのブラウザをサポートする方法
  2. 高フレームレートを維持する方法

この記事では、すべてのブラウザに対応するうえでサポートする必要がある API と、これらのイベントを効率的に使用する方法について説明します。

ジェスチャーで何を行うかに応じて、ユーザーが一度に 1 つの要素を操作できるようにするか、複数の要素を同時に操作できるようにするかが考えられます。

この記事では、すべてのブラウザのサポートと、高いフレームレートを維持する方法を示す 2 つの例を見ていきます。

ドキュメントに触れた GIF の例

最初の例では、ユーザーが 1 つの要素を操作できます。この場合、操作が要素自体で最初に開始されている限り、すべてのタッチイベントをその要素に与えることができます。たとえば、スワイプ可能な要素から指を離しても、引き続きその要素を制御できます。

ユーザーにとっては柔軟性が高いものの、UI の操作方法には制限が適用されるため、これは便利です。

要素に触れた GIF の例

ただし、ユーザーがマルチタッチを使用して複数の要素を同時に操作することが想定される場合は、タップを特定の要素に制限する必要があります。

ユーザーにとっては柔軟性が高くなりますが、UI を操作するロジックが複雑になり、ユーザーエラーに対する耐性が低下します。

イベント リスナーを追加する

Chrome(バージョン 55 以降)、Internet Explorer および Edge では、PointerEvents を使用してカスタム ジェスチャーを実装することをおすすめします。

他のブラウザでは、TouchEventsMouseEvents を使用するのが適切です。

PointerEvents の大きな特長は、マウスイベント、タッチイベント、ペンイベントなど、複数のタイプの入力を 1 つのコールバック セットに統合できることです。リッスンするイベントは pointerdownpointermovepointeruppointercancel です。

他のブラウザでは、タッチイベントに対応する touchstarttouchmovetouchendtouchcancel があります。マウス入力に対して同じ操作を実装する場合は、mousedownmousemovemouseup を実装する必要があります。

使用するイベントについて不明な点がある場合は、タップ、マウス、ポインタのイベントの表をご覧ください。

これらのイベントを使用するには、イベントの名前、コールバック関数、ブール値とともに、DOM 要素の addEventListener() メソッドを呼び出す必要があります。このブール値により、他の要素がイベントをキャッチして解釈する機会が得られる前か後にイベントをキャッチするかが決まります。(true は、他の要素の前にイベントを表示することを意味します)。

インタラクションの開始をリッスンする例を次に示します。

// Check if pointer events are supported.
if (window.PointerEvent) {
  // Add Pointer Event Listener
  swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
  // Add Touch Listener
  swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);

  // Add Mouse Listener
  swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}

試してみる

単一要素の操作を処理する

上記の短いコード スニペットでは、マウスイベントの開始イベント リスナーのみを追加しました。これは、マウスイベントは、イベント リスナーが追加された要素にカーソルを合わせた場合にのみトリガーされるためです。

TouchEvents は、タップがどこで発生したかに関係なく、開始後に操作をトラッキングします。PointerEvents は、DOM 要素で setPointerCapture を呼び出した後に、接触が発生した場所に関係なくイベントをトラッキングします。

マウスの移動と終了のイベントについては、ジェスチャー開始メソッドにイベント リスナーを追加し、ドキュメントにリスナーを追加します。つまり、操作が完了するまでカーソルを追跡できます。

これを実装するためのステップは次のとおりです。

  1. すべての TouchEvent リスナーと PointerEvent リスナーを追加します。MouseEvent の場合は、開始イベントのみを追加します。
  2. 開始ジェスチャー コールバック内で、マウスの移動イベントと終了イベントをドキュメントにバインドします。これにより、元の要素でイベントが発生したかどうかにかかわらず、すべてのマウスイベントを受信できます。PointerEvents では、元の要素で setPointerCapture() を呼び出して、以降のイベントをすべて受信する必要があります。次に、ジェスチャーの開始を処理します。
  3. 移動イベントを処理します。
  4. 終了イベントで、ドキュメントからマウス移動と終了リスナーを削除し、操作を終了します。

以下に、移動イベントと終了イベントをドキュメントに追加する handleGestureStart() メソッドのスニペットを示します。

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

試してみる

追加する終了コールバックは handleGestureEnd() です。このコールバックは、次のようにドキュメントから移動イベントと終了イベント リスナーを削除し、操作が終了するとポインタ キャプチャを解放します。

// Handle end gestures
this.handleGestureEnd = function(evt) {
  evt.preventDefault();

  if (evt.touches && evt.touches.length > 0) {
    return;
  }

  rafPending = false;

  // Remove Event Listeners
  if (window.PointerEvent) {
    evt.target.releasePointerCapture(evt.pointerId);
  } else {
    // Remove Mouse Listeners
    document.removeEventListener('mousemove', this.handleGestureMove, true);
    document.removeEventListener('mouseup', this.handleGestureEnd, true);
  }

  updateSwipeRestPosition();

  initialTouchPos = null;
}.bind(this);

試してみる

このパターンで移動イベントをドキュメントに追加すると、ユーザーが要素を操作し、そのジェスチャーを要素の外に移動すると、ドキュメントからイベントを受け取るため、ページ上の場所に関係なくマウスの動きが引き続き取得されます。

次の図は、ジェスチャーの開始時に移動イベントと終了イベントをドキュメントに追加する際のタッチイベントの動作を示しています。

「touchstart」のドキュメントにタッチイベントをバインドする図

タップへの効率的な応答

開始イベントと終了イベントを処理したので、実際にタッチイベントに応答できます。

開始イベントと移動イベントのいずれについても、イベントから xy を簡単に抽出できます。

次の例では、targetTouches が存在するかどうかを確認して、イベントが TouchEvent からのものかどうかを確認します。含まれている場合は、最初の接触から clientXclientY を抽出します。イベントが PointerEvent または MouseEvent の場合は、イベント自体から直接 clientXclientY を抽出します。

function getGesturePointFromEvent(evt) {
    var point = {};

    if (evt.targetTouches) {
      // Prefer Touch Events
      point.x = evt.targetTouches[0].clientX;
      point.y = evt.targetTouches[0].clientY;
    } else {
      // Either Mouse event or Pointer Event
      point.x = evt.clientX;
      point.y = evt.clientY;
    }

    return point;
  }

試してみる

TouchEvent には、タップデータを含む 3 つのリストがあります。

  • touches: 画面上の DOM 要素に関係なく、現在画面上にあるすべてのタップのリスト。
  • targetTouches: イベントがバインドされている DOM 要素上に現在存在する接触のリスト。
  • changedTouches: イベントを発生させる原因となった変化した接触のリスト。

ほとんどの場合、targetTouches を使用すると必要なものがすべて提供されます。(これらのリストの詳細については、タップリストをご覧ください)。

requestAnimationFrame を使用する

イベントのコールバックはメインスレッドで起動されるため、イベントのコールバックで実行されるコードはできるだけ少なくして、フレームレートを高く保ち、ジャンクを防ぎます。

requestAnimationFrame() を使用すると、ブラウザがフレームを描画する直前に UI を更新できます。これにより、イベント コールバックからある程度の作業を進めることができます。

requestAnimationFrame() について詳しくは、こちらをご覧ください。

一般的な実装では、開始イベントと移動イベントから x 座標と y 座標を保存し、移動イベント コールバック内でアニメーション フレームをリクエストします。

このデモでは、最初のタップ位置を handleGestureStart() に保存します(initialTouchPos を探します)。

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if (evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

handleGestureMove() メソッドはイベントの位置を保存し、必要に応じてアニメーション フレームをリクエストします。その際、onAnimFrame() 関数をコールバックとして渡します。

this.handleGestureMove = function (evt) {
  evt.preventDefault();

  if (!initialTouchPos) {
    return;
  }

  lastTouchPos = getGesturePointFromEvent(evt);

  if (rafPending) {
    return;
  }

  rafPending = true;

  window.requestAnimFrame(onAnimFrame);
}.bind(this);

onAnimFrame 値は、呼び出されると UI を変更して移動させる関数です。この関数を requestAnimationFrame() に渡すことで、ページを更新する直前に関数を呼び出すようブラウザに指示します(ページの変更をペイントします)。

handleGestureMove() コールバックでは、最初に rafPending が false かどうかを確認します。false は、最後の移動イベント以降に requestAnimationFrame() によって onAnimFrame() が呼び出されているかどうかを示します。つまり、実行待ちの requestAnimationFrame() は一度に 1 つのみです。

onAnimFrame() コールバックの実行時に、移動したい要素に対して変換を設定してから、rafPendingfalse に更新し、次のタッチイベントで新しいアニメーション フレームをリクエストできるようにします。

function onAnimFrame() {
  if (!rafPending) {
    return;
  }

  var differenceInX = initialTouchPos.x - lastTouchPos.x;
  var newXTransform = (currentXPosition - differenceInX)+'px';
  var transformStyle = 'translateX('+newXTransform+')';

  swipeFrontElement.style.webkitTransform = transformStyle;
  swipeFrontElement.style.MozTransform = transformStyle;
  swipeFrontElement.style.msTransform = transformStyle;
  swipeFrontElement.style.transform = transformStyle;

  rafPending = false;
}

タップ操作でジェスチャーを操作する

CSS プロパティ touch-action を使用すると、要素のデフォルトのタップ動作を制御できます。この例では、touch-action: none を使用して、ブラウザによるユーザーのタップ操作を禁止し、すべてのタッチイベントをインターセプトできるようにしています。

/* Pass all touches to javascript: */
button.custom-touch-logic {
  touch-action: none;
}

touch-action: none の使用は、デフォルトのブラウザ動作をすべて阻止するため、ややわかりにくいオプションです。多くの場合、以下のいずれかのオプションが適切なソリューションです。

touch-action を使用すると、ブラウザに実装された操作を無効にできます。たとえば IE10 以降では、ダブルタップによるズーム操作がサポートされています。touch-actionmanipulation に設定すると、デフォルトのダブルタップ動作を回避できます。

これにより、ダブルタップ ジェスチャーを自身で実装できます。

よく使用される touch-action 値は次のとおりです。

タップ操作パラメータ
touch-action: none ブラウザはタッチ操作を処理しません。
touch-action: pinch-zoom 「touch-action: none」のようなブラウザ操作をすべて無効にします。ただし、「p チ-zoom」はブラウザで引き続き処理されます。
touch-action: pan-y pinch-zoom 垂直スクロールやピンチズーム(画像カルーセルなど)を無効にせずに、JavaScript で水平スクロールを処理できます。
touch-action: manipulation ブラウザによるクリックの遅延を回避するために、ダブルタップ操作を無効にします。スクロールとピンチズームをブラウザまで残します。

旧バージョンの IE をサポートする

IE10 をサポートする場合は、PointerEvents のベンダー プレフィックス付きバージョンを処理する必要があります。

PointerEvents がサポートされているかどうかを確認するには、通常 window.PointerEvent を探しますが、IE10 では window.navigator.msPointerEnabled を探します。

ベンダー プレフィックスを持つイベント名は、'MSPointerDown''MSPointerUp''MSPointerMove' です。

以下の例は、サポートを確認してイベント名を切り替える方法を示しています。

var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';

if (window.navigator.msPointerEnabled) {
  pointerDownName = 'MSPointerDown';
  pointerUpName = 'MSPointerUp';
  pointerMoveName = 'MSPointerMove';
}

// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
  window.PointerEventsSupport = true;
}

詳しくは、Microsoft のアップデートに関する記事をご覧ください。

リファレンス

タップ状態の疑似クラス

クラス 説明
:hover
押された状態のボタン
要素の上にカーソルを置くと入力されます。カーソルを合わせたときに UI が変わると、ユーザーが要素を操作しやすくなります。
フォーカス
フォーカス状態のボタン
ユーザーがページ上の要素間を移動する際に入力します。フォーカス状態により、ユーザーは現在操作している要素を把握できます。また、キーボードを使用して UI を簡単に操作することもできます。
:アクティブ
押された状態のボタン
要素が選択されたとき(ユーザーが要素をクリックまたはタップしたときなど)に入力します。

タッチイベントに関する最終的なリファレンスについては、W3C タッチイベントをご覧ください。

タップ、マウス、ポインタのイベント

これらのイベントは、アプリに新しい操作を追加するための構成要素です。

タップ、マウス、ポインタのイベント
touchstartmousedownpointerdown このメソッドは、指が最初に要素に触れたとき、またはユーザーがマウスダウンをクリックしたときに呼び出されます。
touchmovemousemovepointermove ユーザーが画面上で指を動かすか、マウスでドラッグしたときに呼び出されます。
touchendmouseuppointerup ユーザーが画面から指を離すか、マウスを離したときに呼び出されます。
touchcancel pointercancel このメソッドは、ブラウザがタッチ操作をキャンセルしたときに呼び出されます。たとえば、ユーザーがウェブアプリをタップしてタブを切り替える場合などです。

タッチリスト

各タッチイベントには、次の 3 つのリスト属性が含まれます。

タップイベントの属性
touches タップされている要素に関係なく、画面上の現在のすべてのタップのリスト。
targetTouches 現在のイベントのターゲットである要素で開始された接触のリスト。たとえば、<button> にバインドすると、現在そのボタンに対するタップのみを取得できます。ドキュメントにバインドすると、そのドキュメントに対する現在のすべてのタッチを取得できます。
changedTouches イベントの発生原因となった変更が行われた接触のリスト:
  • touchstart イベントの場合 - 現在のイベントでアクティブになったタッチポイントのリスト。
  • touchmove イベントの場合 -- 最後のイベント以降に移動したタッチポイントのリスト。
  • touchend イベントと touchcancel イベントの場合 -- サーフェスから削除されたばかりのタッチポイントのリスト。

iOS でアクティブ状態のサポートを有効にする

iOS 版 Safari では、デフォルトでは「アクティブ」状態を適用しません。この状態を機能させるには、touchstart イベント リスナーをドキュメントの本文または各要素に追加する必要があります。

iOS デバイスでのみ実行されるように、ユーザー エージェント テストの背後で行う必要があります。

タップ開始を本文に追加すると、DOM 内のすべての要素に適用されるという利点がありますが、ページのスクロール時にパフォーマンスの問題が発生する可能性があります。

window.onload = function() {
  if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
    document.body.addEventListener('touchstart', function() {}, false);
  }
};

または、ページ内のすべての操作可能な要素にタップ開始リスナーを追加して、パフォーマンスに関する懸念を軽減することもできます。

window.onload = function() {
  if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
    var elements = document.querySelectorAll('button');
    var emptyFunction = function() {};

    for (var i = 0; i < elements.length; i++) {
        elements[i].addEventListener('touchstart', emptyFunction, false);
    }
  }
};