サイト上でのタップ操作をサポートする

スマートフォンからパソコンの画面に至るまで、ますます多くの端末でタッチスクリーンが利用可能になっています。 ユーザーが UI を操作した際は、アプリ側でタップ操作に対して直感的に応答する必要があります。

要素の状態を処理する

ウェブページ上の要素をタップまたはクリックしたときに、サイト側でその操作が本当に検知されているか疑問に感じた経験はないでしょうか?

UI の一部をタップまたは操作したときに要素の色が変わるだけでも、ユーザーはサイトが機能しているとわかり安心するものです。 こうした反応によってユーザーのストレスが緩和されるだけでなく、軽快で反応が良いサイトであると感じてもらえます。

DOM 要素は、デフォルト、フォーカス、ホバー、アクティブのいずれかの状態を継承できます。 それぞれの状態に合わせて UI を変更するには、以下に示すように擬似クラス :hover:focus:active にスタイルを適用する必要があります。

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

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

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

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

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

サンプルを見る

状態によって色が異なるボタンの画像

ほとんどのモバイル ブラウザでは、要素がタップされた後の状態として「hover」と「focus」の両方、またはこのどちらかを要素に適用します。

適用するスタイルとユーザーがタップした後の外観については、慎重に検討してください。

注: アンカータグとボタンは、ブラウザによって動作が異なることがあります。そのため、hover 状態のままになる場合もあれば、focus 状態のままになる場合もあることを認識しておいてください。

デフォルトのブラウザ スタイルを無効にする

さまざまな状態に対応したスタイルを追加すると、ほとんどのブラウザではユーザーのタップに応答して独自のスタイルが実装されることに気付くでしょう。 これは主に、モバイル端末が初めてリリースされた当時、:active 状態用のスタイルが用意されていないサイトが多かったことが原因です。結果的に、多くのブラウザでユーザー操作に応答するためにハイライト色やスタイルが追加されました。

大半のブラウザでは outline という CSS プロパティを使用して、フォーカスされた要素の輪郭線を表示しています。 この動作は、以下のようにすると無効にできます。

.btn:focus {
  outline: 0;

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

Safari と Chrome ではタップした要素がハイライト表示されますが、この動作は次のように CSS プロパティ -webkit-tap-highlight-color で無効にできます。

/* 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;
}

サンプルを見る

警告: 上述のようにデフォルト スタイルを無効にするのは、:hover:active:focus の擬似クラスがある場合だけにしてください。

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

UI を作成するときは、要素に対するユーザー操作は有効にしつつ、長押しによるテキスト選択や UI 上でのマウスによるドラッグ操作など、一部のデフォルト動作を無効にしたい場合があります。

これは、CSS プロパティ user-select を使用すると実現できます。ただし、コンテンツに対してこのような処理をすると、要素内のテキストを選択したいと思っているユーザーは、非常にストレスを感じることがあるため注意が必要です。このような変更は、十分に検討したうえで慎重に行ってください。

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);
}

サンプルを見る

注: API の設計上、PointerEvents は 1 回の pointerdown イベントで、マウスとタップの両方のイベントを処理できます。

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

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

TouchEvents はタップが発生した場所にかかわらず、開始されたジェスチャーを追跡します。PointerEvents はタップが発生した場所にかかわらず、開始されたジェスチャーを追跡して、DOM 要素の setPointerCapture を呼び出します。

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

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

  1. すべての TouchEvent リスナと PointerEvent リスナを追加します。MouseEvents には開始イベントのみを追加します。
  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() を追加して、ジェスチャーが完了した際に移動と終了のイベントリスナをドキュメントから削除し、PointerCapture を解放します。

// 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);

サンプルを見る

このパターンに従ってドキュメントに移動イベントを追加すると、ユーザーが要素の操作を開始したあとにジェスチャーの位置が要素外に移った場合でも、ページ上の位置にかかわらずマウス移動を検知できます。これは、ドキュメントからイベントを受信しているためです。

この図は、ジャスチャーが開始した際に移動と終了のイベントをドキュメントに追加した場合の、タップイベントの処理を示しています。


klzzwxh:0048 のドキュメントにタップイベントをバインドした例

効率的にタップに応答する

開始と終了のイベント処理を追加したので、これで実際にタップイベントに応答することができます。

あらゆる開始および移動イベントについて、イベントから x 座標と y 座標を簡単に取得できます。

以下の例では、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 */
touch-action: none;

touch-action: none はデフォルトのブラウザ動作を完全に抑制するため、使用する際は注意が必要です。 多くの場合は、以下のいずれかのオプションを使うとよいでしょう。

touch-action を使用すると、ブラウザに実装されたジャスチャーを無効にできます。たとえば、Internet Explorer バージョン 10 以降では、ダブルタップによるズーム操作がサポートされています。 このデフォルトのダブルタップ動作を無効にするには、manipulation のタップ操作を設定します。

これにより、自身でダブルタップ操作を実装することができます。

以下は、一般的に使用されているタップ操作の値のリストです。

タッチ操作パラメータ
touch-action: none ブラウザではタップ操作を一切処理しません。
touch-action: pinch-zoom `touch-action: none` と同様にすべてのブラウザ操作を無効にします。ただし、`pinch-zoom` は例外で、引き続きブラウザによって処理されます。
touch-action: pan-y pinch-zoom 縦方向のスクロールやピンチズーム操作を無効にせずに、JavaScript で横方向のスクロールを処理します(例: 画像のカルーセル表示)。
touch-action: manipulation ダブルタップ操作を無効にして、ブラウザでのクリック遅延を防止します。 スクロールやピンチズームの処理はブラウザに委ねます。

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

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

通常、PointerEvents のサポート状況を確認するには window.PointerEvent を探しますが、IE10 の場合は window.navigator.msPointerEnabled を探します。

ベンダー プレフィックス付きのイベント名は、'MSPointerDown', 'MSPointerUp' and '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 をホバー状態に変えることで、ユーザーに要素を操作するように促すことができます。
:focus フォーカス状態のボタン ページ上の要素までユーザーがタブで移動したときの状態です。フォーカス状態を使用すると、ユーザーは現在操作している要素を把握でき、キーボードで簡単に UI 操作が行えるようになります。
:active 押された状態のボタン クリックやタップ操作などによって要素が選択されたときの状態です。

タップイベントの正式なリファレンスは、w3 Touch Events で参照することができます。

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

これらのイベントは、新しいジェスチャーをアプリケーションに追加するために必要な要素です。

タッチ、マウス、ポインタのイベント
touchstart, mousedown, pointerdown 初めて要素に指が触れたとき、またはユーザーがマウスでクリックをしたときに発生します。
touchmove, mousemove, pointermove ユーザーがスクリーンを指でなぞったとき、またはマウスをドラッグしたときに発生します。
touchend, mouseup, pointerup ユーザーがスクリーンから指を放したとき、またはマウスを放したときに発生します。
touchcancel pointercancel タップ操作がブラウザによってキャンセルされたときに発生します。たとえばユーザーがウェブアプリをタップしたあとに、タブを移動した場合などです。

タップリスト

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

タップイベント属性
touches 現在画面上にあるすべてのタップのリスト(タップされている要素は問わない)。
targetTouches 現在のイベントの対象である要素上で開始されたタップのリスト。 たとえば <button> にバインドすると、現在のそのボタン上でのタップのみが取得されます。 ドキュメントにバインドすると、現在のドキュメント上のすべてのタップが取得されます。
changedTouches イベントの発生原因となった変化が生じたタップのリスト:
  • touchstart イベント用 - 現在のイベントでアクティブになったばかりのタップポイントのリスト。
  • touchmove イベント用 - 最後のイベント以降に移動したタップポイントのリスト。
  • touchend touchcancel イベント用 - 画面から指が離れたばかりのタップポイントのリスト。

iOS で active 状態をサポートする

iOS 版の Safari では、残念ながらデフォルトで「active」状態を適用できません。適用可能にするには、「document body」または要素ごとに touchstart イベントリスナを追加する必要があります。

これは iOS 端末に特化した処理なので、ユーザー エージェントのテスト後に行ってください。

タッチ開始のリスナを body に追加すると、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);
    }
  }
};