Touch and mouse with hover effects in a web browser
Problem
I use a hover effect on my website for certain links. On iPads and other touch-based devices, this causes the user to press a link twice: first to add the hover effect, and a second time to actually follow the link. This is undesired.
Background
In order to simulate a mouse, browsers such as Webkit mobile fire the following events if a user touches and releases a finger on touch interface like iPad (source: Touch And Mouse on html5rocks.com):
-
touchstart
-
touchmove
-
touchend
- 300ms delay, where the browser makes sure this is a single tap, not a double tap
-
mouseover
-
mouseenter
-
mousemove
- Note: If a
mouseover
,mouseenter
ormousemove
event changes the page content, the following events are never fired.
- Note: If a
-
mousedown
-
mouseup
-
click
It does not seem possible to simply tell the web browser to skip the mouse events.
What's worse, if the mouseover changes the content or DOM state (and the hover effect does so in my case), the click event is never fired, as explained on Safari Web Content Guide - Handling Events, in particular figure 6.4 in One-Finger Events. What exactly a "content change" is, will depend on browser and version. I've found that for iOS 7.0, a change in background colour is not (or no longer?) a content change.
Unrelated to this problem, there is a 300 ms delay to allow the browser time to determine if the user is performing another gesture - in particular, double-tap zooming. This makes the browser feel sluggish if the first action occurs during the mouse events. If you are 100% the user will never zoom or double click, you can use the FastClick library to prevent this delay. (Disabling zooming also helps for modern browsers, but this is an accessibility concern.). Instead I prefer to keep this delay, and use this time to show the hover effect.
Question
What HTML and JavaScript can be used to support a proper hover effect on links for both touch as well as mouse?
Thus:
- It should work with devices that support both touch and mouse.
- No browser detection.
- A link is followed after a single click. No need to click a second time.
- Ideally, the hover effect is also shown after a touch event.
- It works across multiple browsers.
- As much support for native events, no
preventDefault()
if possible. In particular, a touch can be cancelled by moving your finger.
Solution
- Add hover effects on
touchstart
andmouseenter
. - Remove hover effects on
mouseleave
,touchmove
andclick
.
Note that there is no action on touchend
!
This clearly works for mouse events: mouseenter
and mouseleave
(slightly improved versions of mouseover
and mouseout
) are fired.
If the user actually click
s a link, the hover effect is also removed. This ensure that it is removed if the user presses the back button in the web browser.
This also works for touch events: on touchstart the hover effect is added. It is not removed on touchend. It is added again on mouseenter
, and since this causes no content changes (it was already added), the click
event is also fired, and the link is followed without the need for the user to click again!
The 300ms delay that a browser has between a touchstart
event and click
is actually put in good use because the hover effect will be shown during this short time.
If the user decides to cancel the click, a move of the finger will do so just as normal. Normally, this is a problem since no mouseleave
event is fired, and the hover effect remains in place. Thankfully, this can easily be fixed by removing the hover effect on touchmove
.
That's it!
Alternative Solutions
The following are non-solutions, or problematic alternatives:
- browser detection
- Extremely prone to errors. Assumes that a device has either mouse or touch, while a combination of both will become more and more common when touch displays proliferate.
- CSS media detection
- The only reasonable CSS-only solution. Still prone to errors, and still assumes that a device has either mouse or touch, while both are possible.
- Emulate the click event in
touchend
- This will incorrectly follow the link, even if the user only wanted to scroll or zoom, without the intention of actually clicking the link.
- Use a variable to suppress mouse events
- Set a variable in
touchend
, and use it as a if-condition in subsequent mouse events to prevents state changes during those events. The variable is reset in theclick
event. This is a decent solution if you really don't want a hover effect on touch interfaces. Unfortunately, it does not work if atouchend
while no correspondingclick
event is fired (e.g. the user scrolled or zoomed), and the user subsequently tries to following the link with a mouse (i.e on a device with both mouse and touch interface).
Further Reading
- http://jsfiddle.net/macfreek/24Z5M/. Test for yourself in this sandbox.
- Touch And Mouse on html5rocks.com. Great background article.
- Safari Web Content Guide - Handling Events. See in particular figure 6.4, which explains that no further events are fired after a content change during a
mouseover
ormousemove
event.
This topic has been brought up in the followin StackOverflow questions:
- http://stackoverflow.com/questions/3038898/ipad-iphone-hover-problem-causes-the-user-to-double-click-a-link
- http://stackoverflow.com/questions/7573126/ipad-iphone-double-click-problem
- http://stackoverflow.com/questions/8291517/disable-hover-effects-on-mobile-browsers
- http://stackoverflow.com/questions/14289873/how-to-disable-hover-state-of-button-on-mobile