Ghost clicks are not new in mobile browsers, there is already literature on the web and some solutions to get rid of them (see resources at the end), but it is a complex problem and browsers manage them in different ways. So, let’s review the full problem and see how it is possible to address it.
What are ghost clicks ?
In any touch device, mobile browsers emulate click events when the screen is touched. It is useful to be compatible with websites developed for mouse interaction, and it is needed because there are no native touch events such as tap, swipe, double-tap, etc.
One problem is that in all these browsers, a double tap on the screen triggers a zoom in. In that case, no click event should be fired as the user is obviously not “clicking”. As a consequence, after the first tap, the browser must wait a while to be sure that a second tap for zooming is not happening.
So, here is what happens when the screen is taped:
- A touchstart event is fired as soon as a finger touches the screen.
- A touchend event is fired as soon as the finger leaves the screen.
- The browser waits around 300ms for another tap.
- If not, a click event is fired. This is the ghost click.
Issues with ghost clicks
This 300ms delay makes mobile websites to response slowly, and degrades the user experience. So developers tend to avoid listening to these click events, but instead use the touchstart and touchend events to identify user’s taps. Frameworks usually provide a gesture library to simplify this, and extend it to other touch gestures like swipe, pinch, double-tap, etc. For example with Aria Templates: http://ariatemplates.com/mobile/kitchensink/#/gestures
But the click events are still fired, and they can lead to bad situations because even if not used, they can trigger changes on the UI. For example a click on a link will start navigation, a click on an input field will give it focus and will make the virtual keyboard to pop out.
The worst part is that the click event is fired on the DOM element which is located where the touchend event happened. So if the DOM has been changed in the meantime, then a click event is fired on the new element. This is a serious problem for websites made with a client-side templating framework.
Here is a sample which illustrates this use case: http://instant.ariatemplates.com/anonymous/73cff696276a3fd79b6f/5a1657 or http://bit.ly/1sEqWCG
Preventing ghost clicks
- Calling preventDefault() on the touchstart event completely prevents the ghost click. It works in most browsers but has a big drawback: it is no longer possible to start scrolling the page from this DOM element.
- Call preventDefault() on the touchend event also completely prevents the ghost click. No drawbacks here at first sight, but it is only supported by a some browsers.
- If the page is not scalable thanks to proper meta tags (see Trick section at the end of the article), some browsers do not wait 300ms to fire the ghost clicks. They are still fired but faster, so the click event can reliably be used instead of a tap gesture.
The table below illustrates the discrepancies between mobile browsers when it comes to ghost click management. All tests were done with real devices using test pages available at http://mlaval.github.io/ghostclick/
|preventDefault()||Ghost click timing||Ghost click coordinates|
|touchstart||touchend||Scalable page||Not scalable page||Scalable page||Not scalable page|
|Safari Mobile iOS 5.1.1||Yes||Yes||370ms after end||370ms after end||touchstart||touchstart|
|Safari Mobile iOS 6.1.3||Yes||Yes||370ms after end||370ms after end||touchstart||touchstart|
|Safari Mobile iOS 7.1.1||Yes||Yes||370ms after end||370ms after end||touchstart||touchstart|
|Android 2.3.7||Yes||No||410ms after end||410ms after end||touchstart||touchstart|
|Android 4.0.4||Yes||No||300ms after end||10ms after end||touchstart||touchstart|
|Android 4.1.2||Yes||No||300ms after end||300ms after end||touchstart||touchstart|
|Android 4.2.2||Yes||No||300ms after start||10ms after end||touchstart||touchend|
|IE10 Windows Phone 8||No||No||310ms after end||10ms after end||touchend||touchend|
|Blackberry 10||Yes||Yes||260ms after end||10ms after end||touchstart||touchstart|
|Chrome for iOS||Yes||Yes||360ms after end||360ms after end||touchstart||touchstart|
|Chrome for Android||Yes||Yes||300ms after start||10ms after end||touchstart||touchend|
|Firefox for Android||Yes||No||300ms after end||10ms after end||touchstart||touchend|
From these results, three cases can be isolated depending on the browser and on the scalability of the page:
- Ghost click is prevented when calling preventDefault() on the touchend event.
- Fast ghost click mode (~10ms delay)
- Slow ghost click mode (~300ms delay)
The only universal solution is a good clickbuster, which ideally should identify which of the 3 cases is the current one, and then apply the right fix. But such an identification is difficult before the application is actually used, so an approach for a good tap gesture would be:
- Always call preventDefault() on the touchend event.
- After a successful tap, cancel all click events in the same area during the next 500ms.
- Analyze the first click bustings to identify if ghost clicks are prevented (case 1) or if ghost clicks are fast (case 2) is happening. If case 1, simply stop trying to cancel clicks. If case 2, only cancel clicks during 50ms after a tap.
To go further, some articles about ghost clicks:
And some clickbuster implementations:
To make a page not scalable, the following meta header must be added:
<meta name="viewport" content="width=device-width, user-scalable=no">
In addition in IE10, also add this CSS rule to remove the ghost click delay: