Notes

Notes on handling clicks and taps on iOS devices and touch enabled mobile browsers

Edit on GitHub

JavaScript
5 minutes
  • Click handlers don’t work as supposed to on touch enabled browsers. iOS Safari will not fire a click event if it doesn’t consider the element clickable (more on that below), but will work as expected for all elements if you add an event handler for touch events
  • If you only added an event handler for the click event, it’ll work on desktop screens but likely won’t do anything or behave wonky on a mobile screen. (e.g. the event listener not firing or having to tap two times for it to work)
  • Sometimes you have devices with both touch screens and physical keyboards (e.g. a Blackberry phone, or a touchscreen laptop), so you kinda need to always check for both clicks and taps on websites

Safari and click handlers not working

iOS Safari will only trigger click events for elements that

  • are deemed clickable (a link or an input. divs/spans are not considered clickable)
  • have the CSS cursor property set to pointer
  • have an onclick attribute (can be empty, doesn’t matter)

It’s a known browser bug for iOS Safari that has been around since 2010. It has been documented here, here, here and here. Here is a codepen demo of the issue.

Alternatively, you can listen to touch events instead of clicks. Preferably listen to both because we do have devices with both physical keyboards and touch screens. touchstart, touchend are two touch events. mouseover and mouseout are fallbacks for hovering and cursor movement.

iOS Safari translates taps to a regular click event. It fires mouseover, mousemove, mousedown, mouseup, and click in that order. These events all fire together as soon as the user lifts the finger more on safari mouse events

Touch Events

You can listen to four kinds of touch events

  • touchstart fired when user makes contact with touch surface and a touch point is created
  • touchmove fired when user moves a touch point along the touch surface
  • touchend fired when touch point is removed from surface
  • touchcancel fired when touch point has been disrupted (depends on the implementation, e.g. too many touch points created..)

touchenter and touchleave were proposed drafts that haven’t been implemented

Preventing default behaviour

There is some browser specific stuff that can happen on touch events. For example:

  • Double-tap or pinch-to-zoom
  • Scrolling
  • Going to a link
  • Submitting a form

The solution to prevent all these defaults (and using your custom action) is event.preventDefault()

Handling multiple events

The thing with handling multiple events like touchstart and mouseup is that they’ll fire twice in touch enabled browsers.

In jQuery, you can use the .on() function that is capable of handling multiple events as well as event delegation

1$( "#dataTable tbody" ).on( "touchend click", "tr", function() {
2  alert( $(this).text() );
3});
  • touch event should be handled before click event
  • You can cancel only the default action with .preventDefault()
  • .preventDefault() can also be used to mark an event as handled
  • Returning false prevents the event from bubbling up
  • Returning false from an event handler will automatically call event.stopPropagation() and event.preventDefault(). A false value can also be passed for the handler as a shorthand for function(){ return false; }
1$(this).on("touchstart click", function() {
2  // Do the magic
3  return false; // stop propagation, you've already handled it once
4});

If the listener has already responded to either one of the events, it’ll stop propagation.

In pure JavaScript, you can do something like below:

1<button onclick="startup()">Initialize</button>
1function startup() {
2  var el = document.getElementsByTagName("canvas")[0];
3  el.addEventListener("touchstart", handleStart, false);
4  el.addEventListener("touchend", handleEnd, false);
5  el.addEventListener("touchcancel", handleCancel, false);
6  el.addEventListener("touchmove", handleMove, false);
7  log("initialized.");
8}

onclick attributes vs. event listeners

While adding onlclick attributes to elements may make them clickable on iOS Safari and be considered an easy fix, it is recommended that you keep your JS and HTML separate.

Instead of making elements clickable by adding cursor values in CSS or onclick attributes in HTML, you can do it all in JS only by adding an event listener that handles clicks as well as taps. Touch events work in mobile Safari regardless of whether the element is clickable or not.

1// using jQuery and .on()
2$('.button-group').on('mouseup touchstart', 'button.btn-wishlist', function (e) {
3  let product = $(this).attr('data-product')
4  wishlist.add(product);
5  e.preventDefault();
6});

Adding event listeners instead of using onclick is the preferred way. If you have separate files for HTML and JS, your code should be separate too. Separation of concerns is recommended.

The added benefit of removing the onclick attributes from the buttons was the code becoming more DRY (Don’t Repeat Yourself). Imagine you have a table with 100 cells and you want to do something when any of the cells is clicked. Are you seriously going to add 100 onclick attributes? With a single event handler you can handle clicks on all those cells (because: event delegation).

Tap delays

Mobile browsers used to add a 300-350ms delay between touchend and click and waited to see if it was going to be a double-tap or not, since double-tap was a gesture to zoom into text. This has been removed. As of March 2016 and iOS 9.3, this delay is gone for mobile-optimised sites. What you do need to do is add a <meta> tag to the site’s <header>.

1<meta name="viewport" content="width=device-width">

This sets the viewport width to the same as the device, and is generally a best-practice for mobile-optimised sites. If you are using or have used Bootstrap, this meta tag is usually included in the getting started code.