Categories:

4 novel ways to deal with sticky :hover effects on mobile devices

Created: July 20th, 16'

CSS's venerable :hover pseudo class forms the backbone of many CSS effects, triggered when the mouse rolls over an element on the page. In today's changing landscape however where touch screen inputs share center stage with the mouse, this has presented a bit of a conundrum for webmasters. Touch based devices in an effort to not be left out in the cold with such a pervasive CSS feature do respond to hover, but in the only way that's possible for them, on "tap" versus an actual "hover". While this is overall a good thing, it leads to what's known as the "sticky hover" issue on these devices, where the :hover style stays with the element the user just tapped on until he/she taps again elsewhere in the document, or in some circumstances, reloads the page before the effect is dismissed. This "always on" hover effect is benign in some cases, but a nuisance or even detrimental to the user experience in others. Take for example a set of "volumn" buttons on the page with a "hover" effect that changes their background color- for mouse users, the effect informs the user that the buttons can be interacted with when the mouse rolls over them, but on touch devices where the background color "sticks" to the buttons on tap, it misleads users into thinking the volume is continuously increasing or decreasing after single a tap.

In this tutorial, we'll look at 4 different ways to disable or modify the default :hover effect on mobile devices for a better user experience across platforms. We'll start with the most conservative approach before venturing into something much more ambitious that also accounts for hybrid devices that support both touch screen and the mouse/touchpad for input, and in real time. Lets get rolling.

Method 1- Conditionally add a "non-touch" CSS class to the document root element

First up, a conservative approach that restricts CSS :hover styles to supposedly just mouse based devices (ie: desktops), by adding an arbitrary CSS class (ie: "non-touch") to the root <html> element when the device is deemed as not supporting "touch". A common way is to use JavaScript to make that determination:

var touchsupport = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)
if (!touchsupport){ // browser doesn't support touch
	document.documentElement.className += " non-touch"
}

With the "non-touch" class now in place only for devices that return false when testing for touch support using JavaScript , we then modify our :hover related styles to only target those devices:

html.nontouch nav a:hover{ /* hover effect only visible to devices that report back as not supporting touch */
	background: yellow;
}

This approach is simple and succinct, though it's not without its pitfalls. Detecting for touch support accurately is notoriously difficult using current browsers' APIs, and there will be instances where browsers that don't support touch (or visa versa) will report the opposite instead. Overall, however, this approach does meet the threshold of being "good enough" where the :hover effect doesn't affect the user experience -positively or negatively- that much anyway across platforms.

Method 2- Conditionally add a "can-touch" CSS class to the document root element

The inverse of the 1st method, this approach lets you leave your original :hover styles alone, and instead craft customized :hover styles targeting touch devices on top of them. We'll make use of JavaScript's "touchstart" event, which is invoked whenever the user makes contact with the screen on touch enabled devices, to first determine in real time that the device does in fact support touch input before adding a "can-touch" class to the document root element.

<script>
document.addEventListener('touchstart', function addtouchclass(e){ // first time user touches the screen
	document.documentElement.classList.add('can-touch') // add "can-touch" class to document root using classList API
	document.removeEventListener('touchstart', addtouchclass, false) // de-register touchstart event
}, false)
</script>

The very first time the user touches the screen, the CSS class "can-touch" is added to the root element, indicating the device is touch based. We make use of the classList API- which enjoys excellent support on mobile browsers- to more elegantly add the class to the element. To prevent the action from being performed beyond once, we deregister the assigned function from the event immediately afterwards.

With this set up, we can define our initial :hover styles as normal, then undo or modify it for touch devices afterwards, for example:

ul li a{
	padding: 10px;
	display: block;
}

ul li a:hover{
	background: yellow;
}

html.can-touch ul li a:hover{
	background: none; /* disable hover effect on touch devices */
}

This approach is arguably more accurate than the first in separating touch and non touch devices, though it does require the user to touch the screen first before it kicks in. For dealing with :hover effects that occur on demand anyway, it works, though depending on the differences in styles between the normal and "can-touch" :hover classes, a brief shift in the page's layout may occur as the later is applied to the page on demand. Also, note that touch events such as "touchstart" are not supported on all mobile browsers, IE and Firefox mobile conspicuously being two of them.

Method 3- Using CSS Media Queries Level 4 Interaction Media Features

CSS Media Queries Level 4 adds support for discerning the user's input device capabilities. For our purpose we're interested in "pointer" and "hover", which tells us the level of precision of the user's primary input device and to what degree it supports hover. Take a look at the following CSS media queries and the type of input devices that they help isolate:

@media (pointer:coarse) {
	/* Primary Input is a coarse pointer device such as touchscreen or XBox Kinect etc */
}

@media (pointer:fine) {
	/* Primary Input is a fine pointer device such as a mouse or stylus */
}

@media (hover:none) {
	/* Primary Input doesn't respond to hover at all, even partially (ie: there is no pointing device) */
}

@media (hover:on-demand) {
	/* Primary Input responds to hover only via emulation, such as touch screen devices */
}

@media (hover:hover) {
	/* Primary Input responds to hover fully, such as a mouse or a Nintendo Wii controller */
}

You could for example, define your normal :hover styles inside the media query (@media hover:hover{}) to restrict them to devices that support :hover fully (ones equip with a mouse or certain pointing devices):

@media (hover:hover) {
	nav a:hover{
		background: yellow;
	}
}

or for a more progressive approach that leaves your original :hover styles untouched, target devices that don't support :hover completely:

@media (hover:none), (hover:on-demand) {
	nav a:hover{ /* suppress hover effect on devices that don't support hover fully
		background: none;
	}
}

All of the above logic can also be packaged in JavaScript using window.matchMedia(), such as:

var nofullhover = window.matchMedia("(hover:none), (hover:on-demand)").matches //returns true or false

While you may think you've arrived at the holy grail of detecting for :hover support- using CSS Media Queries Level 4- the reality doesn't quite yet align with its potential. First is the spotty browser support for Media Queries Level 4 Interaction Media Features. At present no Firefox browser (up to FF 50) supports it, which pretty much renders this approach unpractical until things improve in that area. Secondly, the current specs for Media Queries Level 4 Interaction Media Features offer little advantage in my opinion over the proceeding two methods of handling :hover behavior across platforms, other than its elegance and no JavaScript reliance. All 3 methods, however accurate they are in their detection of touch versus no touch support, overlook an increasingly popular setup where the device supports both touchscreen and mouse/ trackpad. In such instances, none of the aforementioned detection schemes are able to determine which input the user is currently using in real time, but rather, merely look at what they deem as the primary input of the device when reporting back the device as either "touch" or "no touch/ mouse". This means a laptop with both touch and mouse inputs will always be pigeonholed as a touch device in most cases, and occasionally depending on the specific set up, a "mouse" device, forcing our :hover related styles to cater to only one of those inputs, regardless of what input the user is currently using. This is obviously a major shortcoming, one that leads to my final detection method below.

Method 4- Dynamically add or remove a "can-touch" class based on current user input type

For this final method, I took it on as a challenge to come up with a way to determine whether the user is using a touch or mouse/trackpad based input in real time. As mentioned, all previous detection methods we've seen so far are static when passing judgment on what input type the user is currently using; when confronted with a hybrid device that supports both touch and non touched inputs and the fact that the user can switch between these inputs at any time, it's "Houston we have a problem", as they say. With all available browser APIs at the moment only able to tell us what input type(s) the user's device supports but not what he/she is using at the moment, it was time for some off road coding to try and circumvent this limitation.

The basic idea behind checking in real time the user's input type is simple enough, with the devil turning out to be in the details. We already have half of the magic formula in Method 2 above, where we turn to JavaScript's "ontouchstart" event handler to be informed when the user has made contact with the screen (and hence is using touch at that moment). But what about when the user switches over to a mouse/track pad? The first thought naturally is to enlist one of JavaScript's mouse related events - "mouseover", "mousemove", "mouseenter" etc - to help us make the call, but the excitement is short-lived once you realize that touch screen devices also respond to mouse events (much like CSS :hover) whenever the user taps on the screen, eroding the distinction between all of these events on touch devices.

So apparently trying to tell when a mouse event was triggered by an actual mouse/ trackpad versus a touch on a touchscreen is much harder than meets the eye, though all is not lost. What I've found is that when both a "touchstart" and mouse event such as "mouseover" are registered on the page, the order of the two events firing when the user touches the screen is consistently the former first followed by the later event:

document.addEventListener('touchstart', functionref, false) // on user tap, "touchstart" fires first
document.addEventListener('mouseover', functionref, false) // followed by mouse event, ie: "mouseover"

We can take advantage of this predictable sequence of events to distinguish between actual touches on the document versus actual mousing over (ie: on a hybrid device), by having the first event whenever fired temporarily block the second to indicate this is a touch event and to filter out true mouseover events at the same time. Lets see how this all comes together to create code that dynamically adds or removes a "can-touch" class to the document root to reflect the current input type of the user at this moment:

<script>

;(function(){
	var isTouch = false //var to indicate current input type (is touch versus no touch) 
	var isTouchTimer 
	var curRootClass = '' //var indicating current document root class ("can-touch" or "")
	
	function addtouchclass(e){
		clearTimeout(isTouchTimer)
		isTouch = true
		if (curRootClass != 'can-touch'){ //add "can-touch' class if it's not already present
			curRootClass = 'can-touch'
			document.documentElement.classList.add(curRootClass)
		}
		isTouchTimer = setTimeout(function(){isTouch = false}, 500) //maintain "istouch" state for 500ms so removetouchclass doesn't get fired immediately following a touch event
	}
	
	function removetouchclass(e){
		if (!isTouch && curRootClass == 'can-touch'){ //remove 'can-touch' class if not triggered by a touch event and class is present
			isTouch = false
			curRootClass = ''
			document.documentElement.classList.remove('can-touch')
		}
	}
	
	document.addEventListener('touchstart', addtouchclass, false) //this event only gets called when input type is touch
	document.addEventListener('mouseover', removetouchclass, false) //this event gets called when input type is everything from touch to mouse/ trackpad
})();

</script>

Demo: Dynamic CSS :hover demonstration

Try out the live demo above on a desktop, touch device, and hybrid device to see how the CSS :hover effect becomes dormant whenever touch is used to interact with the links, but active again if the mouse or trackpad is used instead. Lets break down how it works:

  • We register two events, "touchstart" and "mouseover" on the document to capture both types of events when the user interacts with the page.
  • On non touch devices such as desktops, only the "mouseover" event is ever triggered. The function removetouchclass() is called though does nothing, as the isTouch and curRootClass variables will always be false and "" (empty string), respectively. In other words, no "can-touch" class is ever added to the document.
  • On touch devices (including hybrid devices when the user is currently using "touch"), both the "touchstart" and "mouseover" events are triggered on touch, the former followed by the later.
  • Whenever the user touches the screen, function addtouchclass() is called that sets the isTouch variable to true to indicate the current input type as touch before adding a "can-touch" CSS class to the doument root. To prevent function removetouchclass() from also being called during the same touch action and undoing what was just done, we add the following line to addtouchclass() to maintain the state of the isTouch variable as true for 500 milliseconds:

    isTouchTimer = setTimeout(function(){isTouch = false}, 500)

    With the above pivotal line of code, when removetouchclass() also tries to run immediately following "touchstart" (as touch devices also respond to mouse events) , it is blocked, as the value of isTouch will still be true at that point. Shortly after, the isTouch variable resets itself back to false again after each touch action to ensure isTouch isn't stuck forever in the true state after a touch input is used, and can continue to react to a possible mouse/trackpad input if the user switches to it afterwards (such as on a hybrid device), and call removetouchclass() accordingly.

All this culminates into a "can-touch" class being added or removed from the document in real time to reflect the input device the user is currently using. From my testing it seems to work predictably across every device I throw at it that support JavaScript touch events, though leave a comment below if you notice otherwise.

Conclusion

In this tutorial we looked at 4 different ways to tackle the sticky :hover issue on mobile devices, coming from different angles to detect when the user's input device is touch versus non touch. Until touch devices support actual hover (perhaps using the camera to detect when the finger is lingering over an element), they allow us to craft a better :hover experience across platforms with varying degrees of accuracy and ease of implementation. In many cases, it's better than doing nothing.

Partners