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 functionremovetouchclass()
is called though does nothing, as theisTouch
andcurRootClass
variables will always befalse
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 theisTouch
variable totrue
to indicate the current input type as touch before adding a "can-touch
" CSS class to the doument root. To prevent functionremovetouchclass()
from also being called during the same touch action and undoing what was just done, we add the following line toaddtouchclass()
to maintain the state of theisTouch
variable as true for 500 milliseconds:
isTouchTimer = setTimeout(function(){isTouch = false}, 500)
With the above pivotal line of code, whenremovetouchclass()
also tries to run immediately following "touchstart
" (as touch devices also respond to mouse events) , it is blocked, as the value ofisTouch
will still be true at that point. Shortly after, theisTouch
variable resets itself back tofalse
again after each touch action to ensureisTouch
isn't stuck forever in thetrue
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 callremovetouchclass()
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.