Add Touch to Your Site

Implement Custom Gestures

If you have an idea for custom interactions and gestures for your site there are two topics to keep in mind, how do I support the range of mobile browsers and how do I keep my frame rate high. In this article we'll look at exactly this.

Respond to Touch Input Using Events

Depending on what you would like to do with touch, you’re likely to fall into one of two camps:

  • I want the user to interact with one particular element.
  • I want the user to interact with multiple elements at the same time.

There are trade offs to be had with both.

If the user will only be able to interact with one element, you might want all touch events to be given to that one element, as long as the gesture initially started on the element itself. For example, moving a finger off the swipable element can still control the element.

Example GIF of touch on document

If, however, you expect users to interact with multiple elements at the same time (using multi-touch), you should restrict the touch to the specific element.

Example GIF of touch on element

TL;DR

  • For full device support, handle touch, mouse and Pointer Events.
  • Always bind start event listeners to the element itself.
  • If you want the user to interact with one particular element, bind your move and end listeners to the document in the touchstart method; ensure you unbind them from the document in the end listener.
  • If you want to support multi-touch, either restrict move and end touch events to the element itself or handle all the touches on an element.

Add Event Listeners

Touch events and mouse events are implemented on most mobile browsers.

The event names you need to implement are touchstart, touchmove, touchend and touchcancel.

For some situations, you may find that you would like to support mouse interaction as well; which you can do with the mouse events: mousedown, mousemove, and mouseup.

For Windows Touch devices, you need to support Pointer Events which are a new set of events. Pointer Events merge mouse and touch events into one set of callbacks. This is currently only supported in Internet Explorer 10+ with the prefixed events MSPointerDown, MSPointerMove, and MSPointerUp and in IE 11+ the unprefixed events pointerdown, pointermove, and pointerup.

Touch, mouse and Pointer Events are the building blocks for adding new gestures into your application (see Touch, mouse and Pointer events).

Include these event names in the addEventListener() method, along with the event’s callback function and a boolean. The boolean determines whether you should catch the event before or after other elements have had the opportunity to catch and interpret the events (true means we want the event before other elements).

    // Check if pointer events are supported.
    if (window.PointerEventsSupport) {
      // Add Pointer Event Listener
      swipeFrontElement.addEventListener(pointerDownName, this.handleGestureStart, true);
    } else {
      // Add Touch Listener
      swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
    
      // Add Mouse Listener
      swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
    }
    
View full sample

This code first checks to see if Pointer Events are supported by testing for window.PointerEventsSupport, if Pointer Events aren’t supported, we add listeners for touch and mouse events instead.

The value window.PointerEventSupport is determined by looking for the existence of window.PointerEvent or the now deprecated window.navigator.msPointerEnabled objects. If they are supported we use varibles for event names, which use the prefixed or unprefixed versions depending on the existence of window.PointerEvent.

    var pointerDownName = 'MSPointerDown';
    var pointerUpName = 'MSPointerUp';
    var pointerMoveName = 'MSPointerMove';

    if(window.PointerEvent) {
      pointerDownName = 'pointerdown';
      pointerUpName = 'pointerup';
      pointerMoveName = 'pointermove';
    }
    
    // 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;
    }
    
View full sample

Handle Single-Element Interaction

In the short snippet of code above you may have noticed that we only add the starting event listener, this is a conscious decision.

By adding the move and end event listeners once the gesture has started on the element itself, the browser can check if the touch occured in a region with a touch event listener and if it’s not, can handle it faster by not having to run any additional javascript.

The steps taken to implement this are:

  1. Add the start events listener to an element.
  2. Inside your touch start method, bind the move and end elements to the document. The reason for binding the move and end events to the document is so that we receive all events regardless of whether they occur on the original element or not.
  3. Handle the move events.
  4. On the end event, remove the move and end listeners from the document.

Below is a snippet of our handleGestureStart method which adds the move and end events to the document:

    // 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.PointerEventsSupport) {
        // Pointer events are supported.
        document.addEventListener(pointerMoveName, this.handleGestureMove, true);
        document.addEventListener(pointerUpName, this.handleGestureEnd, true);
      } else {
        // Add Touch Listeners
        document.addEventListener('touchmove', this.handleGestureMove, true);
        document.addEventListener('touchend', this.handleGestureEnd, true);
        document.addEventListener('touchcancel', this.handleGestureEnd, true);
    
        // Add Mouse Listeners
        document.addEventListener('mousemove', this.handleGestureMove, true);
        document.addEventListener('mouseup', this.handleGestureEnd, true);
      }
    
      initialTouchPos = getGesturePointFromEvent(evt);

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

The end callback we add is handleGestureEnd which removes the move and end events from the document when the gesture has finished:

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

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

      isAnimating = false;
    
      // Remove Event Listeners
      if (window.PointerEventsSupport) {
        // Remove Pointer Event Listeners
        document.removeEventListener(pointerMoveName, this.handleGestureMove, true);
        document.removeEventListener(pointerUpName, this.handleGestureEnd, true);
      } else {
        // Remove Touch Listeners
        document.removeEventListener('touchmove', this.handleGestureMove, true);
        document.removeEventListener('touchend', this.handleGestureEnd, true);
        document.removeEventListener('touchcancel', this.handleGestureEnd, true);
    
        // Remove Mouse Listeners
        document.removeEventListener('mousemove', this.handleGestureMove, true);
        document.removeEventListener('mouseup', this.handleGestureEnd, true);
      }
    
      updateSwipeRestPosition();
    }.bind(this);
    
View full sample

Mouse events follow this same pattern since it’s easy for a user to accidentally move the mouse outside of the element, which results in the move events no longer firing. By adding the move event to the document, we’ll continue to get mouse movements regardless of where they are on the page.

You can use the "Show potential scroll bottlenecks" feature in Chrome DevTools to show how the touch events behave:

Enable Scroll Bottleneck in DevTools

With this enabled you can see where touch events are bound and ensure your logic for adding and removing listeners is working as you’d expect.

Illustrating Binding Touch Events to Document in touchstart

Handle Multi-Element Interaction

If you expect your users to use multiple elements at once, you can add the move and end events listeners directly to the elements themselves. This applies to touch only, for mouse interactions you should continue to apply the mousemove and mouseup listeners to the document.

Since we only wish to track touches on a particular element, we can add the move and end listeners for touch and pointer events to the element straight away:

    // Check if pointer events are supported.
    if (window.PointerEventsSupport) {
      // Add Pointer Event Listener
      elementHold.addEventListener(pointerDownName, this.handleGestureStart, true);
      elementHold.addEventListener(pointerMoveName, this.handleGestureMove, true);
      elementHold.addEventListener(pointerUpName, this.handleGestureEnd, true);
    } else {
      // Add Touch Listeners
      elementHold.addEventListener('touchstart', this.handleGestureStart, true);
      elementHold.addEventListener('touchmove', this.handleGestureMove, true);
      elementHold.addEventListener('touchend', this.handleGestureEnd, true);
      elementHold.addEventListener('touchcancel', this.handleGestureEnd, true);

      // Add Mouse Listeners
      elementHold.addEventListener('mousedown', this.handleGestureStart, true);
    }
    
View full sample

In our handleGestureStart and handleGestureEnd function, we add and remove the mouse event listeners to the document.

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

              var point = getGesturePointFromEvent(evt);
      initialYPos = point.y;
    
      if (!window.PointerEventsSupport) {
        // Add Mouse Listeners
        document.addEventListener('mousemove', this.handleGestureMove, true);
        document.addEventListener('mouseup', this.handleGestureEnd, true);
      }
    }.bind(this);

    this.handleGestureEnd = function(evt) {
      evt.preventDefault();
    
      if(evt.targetTouches && evt.targetTouches.length > 0) {
        return;
      }
    
      if (!window.PointerEventsSupport) {
        // Remove Mouse Listeners
        document.removeEventListener('mousemove', this.handleGestureMove, true);
        document.removeEventListener('mouseup', this.handleGestureEnd, true);
      }

      isAnimating = false;
      lastHolderPos = lastHolderPos + -(initialYPos - lastYPos);
    }.bind(this);
    
View full sample

60fps while Using Touch

Now that we have the start and end events taken care of we can actually respond to the touch events.

Get and Store Touch Event Coordinates

For any of the start and move events, you can easily extract x and y from an event.

The following code snippet checks whether the event is from a touch event by looking for targetTouches, if it is then it extracts the clientX and clientY from the first touch. If the event is a mouse or pointer event then we extract clientX and clientY directly from the event itself.

    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;
      }
    
View full sample

Each touch event has three lists containing touch data (see also Touch lists):

  • touches: list of all current touches on the screen, regardless of DOM element they are on.
  • targetTouches: list of touches currently on the DOM element the event is bound to.
  • changedTouches: list of touches which changed resulting in the event being fired.

In most cases, targetTouches gives you everything you need.

requestAnimationFrame

Since the event callbacks are fired on the main thread, we want to run as little code as possible in the callback to keep our frame rate high, preventing jank.

Use requestAnimationFrame to change the UI in response to an event. This gives you an opportunity to update the UI when the browser is intending to draw a frame and will help you move some work out of your callback.

A typical implementation is to save the x and y coordinates from the start and move events and request an animation frame in the move event callback.

In our demo, we store the initial touch position in 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.PointerEventsSupport) {
        // Pointer events are supported.
        document.addEventListener(pointerMoveName, this.handleGestureMove, true);
        document.addEventListener(pointerUpName, this.handleGestureEnd, true);
      } else {
        // Add Touch Listeners
        document.addEventListener('touchmove', this.handleGestureMove, true);
        document.addEventListener('touchend', this.handleGestureEnd, true);
        document.addEventListener('touchcancel', this.handleGestureEnd, true);
    
        // Add Mouse Listeners
        document.addEventListener('mousemove', this.handleGestureMove, true);
        document.addEventListener('mouseup', this.handleGestureEnd, true);
      }
    
      initialTouchPos = getGesturePointFromEvent(evt);

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

The handleGestureMove method stores the y position before requesting an animation frame if we need to, passing in our onAnimFrame function as the callback:

    var point = getGesturePointFromEvent(evt);
    lastYPos = point.y;
    
      if(isAnimating) {
        return;
      }

      isAnimating = true;
      window.requestAnimFrame(onAnimFrame);
    
View full sample

It’s in the onAnimFrame function that we change our UI to move the elements around. Initially we check to see if the gesture is still on-going to determine whether we should still animate or not, if so we use our initial and last y positions to calculate the new transform for our element.

Once we’ve set the transform, we set the isAnimating variable to false so the next touch event will request a new animation frame.

    function onAnimFrame() {
        if(!isAnimating) {
          return;
        }
    
        var newYTransform = lastHolderPos + -(initialYPos - lastYPos);
    
        newYTransform = limitValueToSlider(newYTransform);
    
        var transformStyle = 'translateY('+newYTransform+'px)';
        elementHold.style.msTransform = transformStyle;
        elementHold.style.MozTransform = transformStyle;
        elementHold.style.webkitTransform = transformStyle;
        elementHold.style.transform = transformStyle;
    
        isAnimating = false;
      }
    
View full sample

Control Gestures using Touch Actions

The CSS property touch-action allows you to control the default touch behavior of an element. In our examples, we use touch-action: none to prevent the browser from doing anything with a users touch, allowing us to intercept all of the touch events.

    /* Pass all touches to javascript */
    -ms-touch-action: none;
    touch-action: none;
    
View full sample

touch-action allows to disable gestures implemented by a browser, for example IE10+ supports double-tap zoom and by setting a touch-action of pan-x | pan-y | manipulation you are preventing the double-tap behavior.

The benefit of this is that it allows you to implement these gestures yourself, but in the case of IE10+ you also remove the 300ms click delay.

Below is a list of the available parameters for touch-action.

Property Description
touch-action: auto The browser will add the normal touch interactions which it supports. For example, scrolling in the x-axis, scrolling in the y-axis, pinch zoom and double tap.
touch-action: none No touch interactions will be handled by the browser.
touch-action: pan-x Only horizontal scrolling will be handled by the browser; vertical scrolling and gestures will be disabled.
touch-action: pan-y Only vertical scrolling will be handled by the browser; horizontal scrolling and gestures will be disabled.
touch-action: manipulation Scrolling in both directions and pinch zooming will be handled by the browser; all other gesture will be ignored by the browser.

Remember

  • Using touch-action: pan-x or touch-action: pan-y are great for being explicit in your intention that a user should only ever scroll vertically or horizontally on an element.

Reference

The definitive touch events reference can be found here: w3 Touch Events.

Touch, Mouse, and Pointer events

These events are the building blocks for adding new gestures into your application:

Touch, Mouse, Pointer Events Description
touchstart, mousedown, pointerdown This is called when a finger first touches an element or when the user clicks down on the mouse.
touchmove, mousemove, pointermove This is called when the user moves their finger across the screen or drags with the mouse.
touchend, mouseup, pointerup This is called when the user lifts their finger off of the screen or releases the mouse.
touchcancel This is called when the browser cancels the touch gestures.

Touch Lists

Each touch event includes three list attributes:

Attribute Description
touches List of all current touches on the screen, regardless of elements being touched.
targetTouches List of touches that started on the element that is the target of the current event. For example, if you bind to a <button>, you'll only get touches currently on that button. If you bind to the document, you'll get all touches currently on the document.
changedTouches List of touches which changed resulting in the event being fired:
  • For the touchstart event-- list of the touch points that just became active with the current event.
  • For the touchmove event-- list of the touch points that have moved since the last event.
  • For the touchend and touchcancel events-- list of the touch points that have just been removed from the surface.

Updated on 2014-01-06

Authors

Matt Gaunt

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. For details, see our Site Policies.