Creating Fast Buttons for h5
Background
Here at Google, we’re constantly pushing the boundaries of what’s possible in a mobile web application. Technologies like HTML5 allow us to blur the distinction between web apps and native apps on mobile devices. As part of that effort we’ve developed a technique for creating buttons that are far more responsive than plain HTML buttons. Previously for a button, or any clickable element, we might have just set up a click handler. For example:
<buttononclick='signUp()'>Sign Up!</button>
The problem with this approach is that mobile browsers will wait approximately 300ms from the time that you tap the button to fire the click event. The reason for this is that the browser is waiting to see if you are actually performing a double tap. For most buttons we are developing we know that there is no double click behavior that we want to handle, so waiting this long to start acting on the click is time wasted for users. We first developed this technique for the Google Voice mobile web app since we wanted users to be able to dial phone numbers rapidly.
Handling Touch Events
The technique involves a bit of JavaScript that allows the button to respond to touchend
events rather than click
events. Touchend
events are fired with no delay so this is significantly faster than click
events, however there are a few problems to consider:
- If the user tapped somewhere else on the screen and then invokes a
touchend
on the button then we should not fire a click. - If the user touches down on the button and then drags the screen a bit and then invokes a
touchend
on the button then we should not fire a click. - We want to highlight the button when the user touches down to give it a pressed state.
We can solve the first two problems by monitoring touchstart
and touchmove
events as well. We should only consider a touchend
on the button if there was previously a touchstart
on the button. Also if there exists a touchmove
anywhere that goes past a certain threshold from the touchstart
then we should not handle the touchend
as a click.
We can solve the third problem by adding an onclick
handler to the button as well. Doing so will allow the browser to properly treat it as a button, and our touchend
handler will ensure that the button is still fast. Also, the presence of an onclick
handler serves as a good fallback for browsers that don’t support touch events.
Busting Ghost Clicks
Adding the onclick
handler back to the button introduces one last ugly problem. When you tap the button, a click event will still be fired 300ms later. Now we run the risk of the click handler executing twice. This can be easily resolved by calling preventDefault
on the touchstart
event. Calling preventDefault
on touchstart
events will stop clicks and scrolling from occurring as a result of the current tap. We wanted users to be able to scroll even if they started their scroll on a button, so we didn’t consider this an acceptable solution. What we came up with to deal with these ghost clicks was called a click buster. What we do is simply add a click event listener to the body, listening on the capture phase. When our listener is invoked, we try to determine if the click was as a result of a tap that we already handled, and if so we call preventDefault
and stopPropagation
on it.
Fast Button Code
Here we will offer some code that implements the ideas we have just discussed.
Construct the FastButton with a reference to the element and click handler.
google.ui.FastButton=function(element, handler){ this.element = element; this.handler = handler;
element.addEventListener('touchstart',this,false); element.addEventListener('click',this,false); };
google.ui.FastButton.prototype.handleEvent =function(event){ switch(event.type){ case'touchstart':this.onTouchStart(event);break; case'touchmove':this.onTouchMove(event);break; case'touchend':this.onClick(event);break; case'click':this.onClick(event);break; } };
Save a reference to the touchstart
coordinate and start listening to touchmove
and touchend
events. Calling stopPropagation
guarantees that other behaviors don’t get a chance to handle the same click event.
google.ui.FastButton.prototype.onTouchStart =function(event){ event.stopPropagation();
this.element.addEventListener('touchend',this,false); document.body.addEventListener('touchmove',this,false);
this.startX =event.touches[0].clientX; this.startY =event.touches[0].clientY; };
When a touchmove
event is invoked, check if the user has dragged past the threshold of 10px.
google.ui.FastButton.prototype.onTouchMove =function(event){ if(Math.abs(event.touches[0].clientX -this.startX)>10|| Math.abs(event.touches[0].clientY -this.startY)>10){ this.reset(); } };
Invoke the actual click handler and prevent ghost clicks if this was a touchend
event.
google.ui.FastButton.prototype.onClick =function(event){ event.stopPropagation(); this.reset(); this.handler(event);
if(event.type =='touchend'){ google.clickbuster.preventGhostClick(this.startX,this.startY); } };
google.ui.FastButton.prototype.reset =function(){ this.element.removeEventListener('touchend',this,false); document.body.removeEventListener('touchmove',this,false); };
Call preventGhostClick
to bust all click events that happen within 25px of the provided x, y coordinates in the next 2.5s.
google.clickbuster.preventGhostClick =function(x, y){ google.clickbuster.coordinates.push(x, y); window.setTimeout(google.clickbuster.pop,2500); };
google.clickbuster.pop =function(){ google.clickbuster.coordinates.splice(0,2); };
If we catch a click event inside the given radius and time threshold then we call stopPropagation
and preventDefault
. Calling preventDefault
will stop links from being activated.
google.clickbuster.onClick =function(event){
for(var i =0; i < google.clickbuster.coordinates.length; i +=2){
var x = google.clickbuster.coordinates[i];
var y = google.clickbuster.coordinates[i +1];
if(Math.abs(event.clientX - x)<25&&Math.abs(event.clientY - y)<25){
event.stopPropagation();
event.preventDefault();
}
}
};
document.addEventListener('click', google.clickbuster.onClick,true);
google.clickbuster.coordinates =[];
Conclusion
At this point you should be able to easily create fast buttons. With a bit of fancy styling you can make them look and feel like native buttons according to the platform you are developing for. There are already a few mobile JavaScript libraries available that solve this same problem, but we haven’t yet seen any that provide the fallback for click events or a solution to the ghost clicks problem. We hope that browser developers will solve this problem in future releases by firing click events immediately when zooming is disabled on the website (using the viewport meta tag). In fact, this is already the case with the Gingerbread Android Browser.